# Tutorial: Reusable Capabilities

Build an editorial agent that can **summarise**, **translate**, and **classify sentiment** — without writing a single body for those action methods. This tutorial walks through the `@Capability` pattern, showing composition, override, and the common mistakes the framework prevents.

## Goal

At the end of the tutorial you will have:

1. Three reusable capability interfaces (`Summarizer`, `Translator`, `SentimentClassifier`) that can be shared across any number of roles.
2. An `EditorRole` that composes all three — **zero method bodies** for dispatched actions.
3. A `StrictEditor` variant that overrides one capability with a deterministic local implementation — demonstrating the role-wins dedupe rule.
4. A compile-backed discovery test that verifies all three actions are present.

## Prerequisites

- [Installation](../start/installation.md)
- An LLM provider configured (any one of Anthropic, OpenAI, Ollama, etc.)
- Familiarity with [Roles](../agents/fundamentals/roles.md) and the [Action System](../agents/fundamentals/action-system.md)
- Recommended: skim [Capabilities](../agents/fundamentals/capabilities.md) first for the conceptual overview

## Step 1 — Define the Capabilities

Each capability is an interface annotated `@Capability`. The methods carry their usual `@ActionSpec` metadata; the default bodies throw `Actions.dispatchedByFramework()` so bypass calls fail loudly instead of silently returning `null`.

Per-action LLM overrides (system prompt, temperature) live directly on `@ActionSpec` — no nested annotation needed.

### `Summarizer`

<!-- java-contract: src/main/java/com/example/tutorial/capabilities/Summarizer.java -->

```java
package com.example.tutorial.capabilities;

import com.tnsai.actions.Actions;
import com.tnsai.annotations.ActionSpec;
import com.tnsai.capabilities.Capability;
import com.tnsai.enums.ActionType;

@Capability
public interface Summarizer {

    @ActionSpec(
            type = ActionType.LLM,
            description = "Summarise the given text in one short paragraph of at most 60 words.",
            llmSystemPrompt = "You are a concise summariser. Output plain text only, no preamble.",
            llmTemperature = 0.2f)
    default String summarize(String text) {
        throw Actions.dispatchedByFramework();
    }
}
```

### `Translator`

<!-- java-contract: src/main/java/com/example/tutorial/capabilities/Translator.java -->

```java
package com.example.tutorial.capabilities;

import com.tnsai.actions.Actions;
import com.tnsai.annotations.ActionSpec;
import com.tnsai.capabilities.Capability;
import com.tnsai.enums.ActionType;

@Capability
public interface Translator {

    @ActionSpec(
            type = ActionType.LLM,
            description = "Translate the given text to the target language, preserving tone.",
            llmSystemPrompt = "You are a precise translator. Output only the translation.",
            llmTemperature = 0.1f)
    default String translate(String text, String targetLanguage) {
        throw Actions.dispatchedByFramework();
    }
}
```

### `SentimentClassifier`

<!-- java-contract: src/main/java/com/example/tutorial/capabilities/SentimentClassifier.java -->

```java
package com.example.tutorial.capabilities;

import com.tnsai.actions.Actions;
import com.tnsai.annotations.ActionSpec;
import com.tnsai.capabilities.Capability;
import com.tnsai.enums.ActionType;

@Capability
public interface SentimentClassifier {

    @ActionSpec(
            type = ActionType.LLM,
            description = "Classify the sentiment as POSITIVE, NEGATIVE, or NEUTRAL.",
            llmSystemPrompt = "Output exactly one of: POSITIVE, NEGATIVE, NEUTRAL.",
            llmTemperature = 0.0f)
    default String classifySentiment(String text) {
        throw Actions.dispatchedByFramework();
    }
}
```

Each capability is now a first-class reusable contract. Any role in your codebase can gain these abilities by `implements`-ing the interface.

## Step 2 — Compose Them Onto a Role

The role class is where agent state lives (memory, history, lifecycle). Capabilities are added by implementing their interfaces — no method bodies needed:

<!-- java-contract: src/main/java/com/example/tutorial/roles/EditorRole.java -->

```java
package com.example.tutorial.roles;

import com.example.tutorial.capabilities.SentimentClassifier;
import com.example.tutorial.capabilities.Summarizer;
import com.example.tutorial.capabilities.Translator;
import com.tnsai.models.role.RoleIdentity;
import com.tnsai.roles.Role;

public final class EditorRole extends Role
        implements Summarizer, Translator, SentimentClassifier {

    @Override
    public RoleIdentity getIdentity() {
        return new RoleIdentity(
                "Editor",
                "Produce clean, readable articles in any target language",
                "editorial");
    }
}
```

`ActionDiscovery` walks the interface chain at role initialisation, picks up the three `@ActionSpec` methods from the capability interfaces, and registers them as the role's actions. The LLM sees all three as tools it can call; dispatch flows through `ActionExecutor` → `LLMRoleExecutor` exactly as it would for a concrete `@ActionSpec` method.

## Step 3 — Wire Into an Agent

<!-- java-contract: src/main/java/com/example/tutorial/TutorialMain.java -->

```java
package com.example.tutorial;

import com.example.tutorial.roles.EditorRole;
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.identity.AgentDescriptor;
import com.tnsai.identity.LocalIdentityProvider;
import com.tnsai.llm.LLMClientFactory;
import com.tnsai.roles.Role;

import java.time.Duration;

public final class TutorialMain {

    private TutorialMain() {
    }

    public static void main(String[] args) {
        var role = Role.create(EditorRole.class);
        var actionNames = ActionDiscovery.discoverActions(role).getActions().stream()
                .map(action -> action.getName())
                .toList();
        var principal = new LocalIdentityProvider().issue(
                AgentDescriptor.builder()
                        .agentClass(EditorRole.class.getName())
                        .systemPrompt("Edit, translate, and classify supplied text.")
                        .toolNames(actionNames)
                        .model("openai:gpt-4o")
                        .build());

        Agent editor = AgentBuilder.create()
                .llm(LLMClientFactory.create("openai", "gpt-4o", 0.3f))
                .role(role)
                .principal(principal)
                .liabilitySink(new RecordingLiabilitySink())
                .authorityScope(AuthorityScope.unrestricted(Duration.ofHours(1)))
                .build();

        String reply = editor.chat(
                "Summarise this in one line, then translate the summary to Turkish: "
                        + "Capabilities eliminate boilerplate by moving dispatched method "
                        + "contracts into reusable interfaces.");
        System.out.println(reply);
    }
}
```

Run it; the LLM picks `summarize`, then picks `translate`, and produces the combined output. Your codebase contains zero lines of `return null;` — the contracts are expressed once, on the capability interfaces.

The principal, liability sink, and authority scope are mandatory for every
`AgentBuilder.build()` call. Use a durable liability sink and a narrower scope
for production deployments.

## Step 4 — Override When You Need Deterministic Behaviour

A role that implements a capability can *also* declare its own version of the method. When that happens, the role's declaration wins; the capability's default is skipped. Use this when a specific role needs deterministic, non-LLM behaviour for one of the contracted actions:

<!-- java-contract: src/main/java/com/example/tutorial/roles/StrictEditor.java -->

```java
package com.example.tutorial.roles;

import com.example.tutorial.capabilities.Summarizer;
import com.tnsai.annotations.ActionSpec;
import com.tnsai.enums.ActionType;
import com.tnsai.models.role.RoleIdentity;
import com.tnsai.roles.Role;

public final class StrictEditor extends Role implements Summarizer {

    @Override
    public RoleIdentity getIdentity() {
        return new RoleIdentity(
                "StrictEditor",
                "Truncate-first summarise with deterministic output",
                "editorial");
    }

    @Override
    @ActionSpec(
            type = ActionType.LOCAL,
            description = "Truncate to the first 60 characters.")
    public String summarize(String text) {
        return text.length() <= 60 ? text : text.substring(0, 60) + "...";
    }
}
```

When `ActionDiscovery` scans `StrictEditor`, it records the class-declared `summarize` signature on the first pass, then skips `Summarizer.summarize` on the capability-interface pass. Exactly one `summarize` action ends up in the role's metadata, and its `ActionType` is `LOCAL` rather than `LLM`. The LLM still sees an action called `summarize`; invoking it just runs the deterministic body instead of hitting the model.

## Step 5 — Test

<!-- java-contract: src/test/java/com/example/tutorial/EditorRoleCapabilityTest.java -->

```java
package com.example.tutorial;

import com.example.tutorial.roles.EditorRole;
import com.tnsai.actions.ActionDiscovery;
import com.tnsai.metadata.ActionMetadata;
import com.tnsai.metadata.DiscoveredRoleActions;
import org.junit.jupiter.api.Test;

import java.util.Set;
import java.util.stream.Collectors;

import static org.junit.jupiter.api.Assertions.assertEquals;

class EditorRoleCapabilityTest {

    @Test
    void editorGainsAllThreeCapabilityActions() {
        DiscoveredRoleActions discovered = ActionDiscovery.discoverActions(EditorRole.class);
        Set<String> actionNames = discovered.getActions().stream()
                .map(ActionMetadata::getName)
                .collect(Collectors.toSet());

        assertEquals(
                Set.of("summarize", "translate", "classifySentiment"),
                actionNames,
                "Role must gain actions from every capability interface");
    }
}
```

`ActionDiscovery.discoverActions` is synchronous and pure — it works at unit-test time without an LLM client. A passing test here proves the composition works before you ever hit the network.

## Common Mistakes the Framework Catches

Two misuses of `@Capability` fail fast at discovery time with an `IllegalStateException`. Knowing them up front saves a debugging round:

1. **Abstract method (no `default` body)** — a capability method without a default forces every adopter to write a body, defeating the whole point. The error message names the offending interface and method, and points to `default { throw Actions.dispatchedByFramework(); }` as the fix.
2. **`ActionResult` parameter on a capability method** — capabilities are pure dispatch. If a method needs post-processing on the LLM's raw response, keep that method on the concrete role class (the legacy pattern) rather than on the capability interface.

The validator message is explicit in both cases; you don't have to guess what went wrong.

## What Not to Turn Into a Capability

- **`ActionType.LOCAL` methods** — they have real bodies the framework invokes via reflection. They don't suffer from the `return null` problem.
- **Methods with `ActionResult` parameters** — rejected by the validator by design.
- **One-off methods used by a single role** — extracting a capability interface for one consumer adds indirection without reuse benefit. Inline is fine.

## Related

- [Capabilities](../agents/fundamentals/capabilities.md) — concept reference, validation rules, migration table
- [Action System](../agents/fundamentals/action-system.md) — how `ActionExecutor` routes dispatched calls
- [Roles](../agents/fundamentals/roles.md) — declaring roles and their `RoleIdentity`

## Implementation References

- `com.tnsai.capabilities.Capability` — marker annotation
- `com.tnsai.actions.Actions.dispatchedByFramework()` — exception-returning helper
- `com.tnsai.actions.ActionDiscovery` — two-pass discovery (class methods, then `@Capability` interface methods)
