# Capabilities

Capabilities are reusable, body-less action contracts. A `@Capability` interface carries one or more `@ActionSpec`-annotated methods that describe *what* the capability does; the framework dispatches the call at runtime. A role gains the capability by `implements`-ing the interface — without writing any method bodies.

This page covers the pattern, composition, override rules, validation, and migration from the legacy `return null;` style.

_Available from 0.3.1. The legacy pattern (concrete methods with `return null;` bodies) was removed in 0.5.0._

## The Problem

Before capabilities, every dispatched `@ActionSpec` method required a `return null;` body:

```java
@ActionSpec(
    type = ActionType.LLM,
    description = "Summarise the text in one short paragraph",
    llmSystemPrompt = "You are concise.",
    llmTemperature = 0.2f
)
public String summarize(String text) {
    return null;   // body unused — LLMRoleExecutor handles dispatch
}
```

The body is dead code: [`ActionExecutor`](action-system.md) skips method invocation entirely for `LLM`/`MCP_TOOL`/`WEB_SERVICE` actions when there is no `ActionResult` parameter. Yet the method declaration forces three problems:

- **Misleading signature** — static analysis marks the method as "always returns null"; downstream `@NonNull` checks inferred from this are bogus.
- **Silent failure on bypass** — anyone who invokes the method outside the dispatch path (direct `role.summarize(x)`, test code, misconfigured filter) gets `null`. This looks identical to a genuine LLM failure and makes debugging painful.
- **Duplicated specification** — every role that wants the same capability copy-pastes the `@ActionSpec` annotation. Drift is inevitable.

## The Pattern

Move the method into a `@Capability` interface. The interface's `default` body throws `Actions.dispatchedByFramework()` — a loud, framework-owned marker that never executes in the normal dispatch path:

```java
@Capability
public interface Summarizer {
    @ActionSpec(
        type = ActionType.LLM,
        description = "Summarise the text in one short paragraph",
        llmSystemPrompt = "You are concise.",
        llmTemperature = 0.2f
    )
    default String summarize(String text) {
        throw Actions.dispatchedByFramework();
    }
}
```

A role picks up the capability by implementing the interface — with no method bodies of its own:

```java
@RoleDeclaration(name = "Editor", goal = "Produce clean, readable articles")
public class EditorRole extends Role implements Summarizer {
    // State, lifecycle, and other non-capability methods live here.
    // No `summarize` body — inherited from Summarizer.
}
```

When the LLM calls `summarize`, `ActionExecutor` discovers the method through the capability interface and routes dispatch through the `LLM` executor — exactly as it would for a concrete `@ActionSpec` method. The default `throw` body is never executed in the normal path; it only fires if someone bypasses dispatch and invokes the method directly, producing a clear `DispatchedByFrameworkException` instead of a silent `null`.

## Composition

A role can implement any number of capability interfaces. Each contributes its actions to the role:

```java
@Capability
public interface Translator {
    @ActionSpec(type = ActionType.LLM, description = "Translate the text to the target language")
    default String translate(String text, String targetLanguage) {
        throw Actions.dispatchedByFramework();
    }
}

@Capability
public interface Classifier {
    @ActionSpec(type = ActionType.LLM, description = "Classify input as POSITIVE / NEGATIVE / NEUTRAL")
    default String classifySentiment(String text) {
        throw Actions.dispatchedByFramework();
    }
}

@RoleDeclaration(name = "Assistant", goal = "Handle ad-hoc requests")
public class AssistantRole extends Role implements Summarizer, Translator, Classifier {
    // Three capabilities, zero bodies. All actions ready for LLM dispatch.
}
```

`ActionDiscovery` walks the role's interface chain — including super-interfaces of capabilities (`MultilingualSummarizer extends Summarizer`) — so every capability contributes its methods regardless of whether it arrives via direct implementation or transitive extension.

## Override — Role Declaration Wins

If a role declares the same signature as a capability's default method, the role's version is what ends up in the discovered action list. Use this to drop dispatch entirely for a specific role and provide a deterministic local implementation:

```java
public class StrictEditor extends Role implements Summarizer {
    // Replaces Summarizer.summarize's LLM dispatch with a deterministic local impl
    @Override
    @ActionSpec(type = ActionType.LOCAL, description = "Truncate-first summarise (deterministic)")
    public String summarize(String text) {
        return text.length() <= 60 ? text : text.substring(0, 60) + "...";
    }
}
```

Discovery sees `summarize` declared on the concrete class first, records its signature, and then skips the capability's default when walking the interface chain. Exactly one `summarize` action ends up in the role's metadata, and its type is `LOCAL` rather than `LLM`.

## Validation

Two rules are enforced at action-discovery time; violations throw `IllegalStateException` with a message naming the offending interface and method.

### Capability methods must be `default`

An abstract method on a `@Capability` interface would force every adopting role to write a body — defeating the point of the annotation. The error message points at the correct body:

```text
@Capability interface com.example.Summarizer method summarize must be a default method.
Abstract capability methods force every adopting role to write a body, defeating the
purpose of the annotation. Use `default { throw Actions.dispatchedByFramework(); }` as the body.
```

### Capability methods must not declare an `ActionResult` parameter

Capabilities are pure dispatch. Post-processing (reading the LLM's raw response, running it through custom logic before returning) belongs on the concrete role class where per-role logic makes sense. The validator rejects the mixed case:

```text
@Capability interface com.example.Summarizer method summarize must not declare an
ActionResult parameter. Capabilities are pure dispatch — move post-processing
(ActionResult-based) to a concrete method on the role class itself.
```

If you want post-processing, keep that specific method off the capability interface and declare it directly on the role (the legacy pattern). Capability-dispatched and role-owned methods coexist on the same role without interference.

## Migration

To migrate a legacy role:

1. For each dispatched `@ActionSpec` method (LLM / MCP / WEB_SERVICE) whose body is `return null;`, extract it to a `@Capability` interface.
2. Give the interface method a `default { throw Actions.dispatchedByFramework(); }` body.
3. On the role class, remove the original method entirely and add `implements YourCapability`.
4. Methods that have a meaningful body — typically because they declare an `ActionResult` parameter for post-processing — stay on the role class unchanged.

### Before

```java
@RoleDeclaration(name = "Editor", goal = "Produce clean articles")
public class EditorRole extends Role {

    @ActionSpec(type = ActionType.LLM, description = "Summarise the text",
                llmSystemPrompt = "You are concise.", llmTemperature = 0.2f)
    public String summarize(String text) {
        return null;
    }

    @ActionSpec(type = ActionType.LLM, description = "Translate to the target language")
    public String translate(String text, String targetLanguage) {
        return null;
    }
}
```

### After

```java
@Capability
public interface Summarizer {
    @ActionSpec(type = ActionType.LLM, description = "Summarise the text",
                llmSystemPrompt = "You are concise.", llmTemperature = 0.2f)
    default String summarize(String text) {
        throw Actions.dispatchedByFramework();
    }
}

@Capability
public interface Translator {
    @ActionSpec(type = ActionType.LLM, description = "Translate to the target language")
    default String translate(String text, String targetLanguage) {
        throw Actions.dispatchedByFramework();
    }
}

@RoleDeclaration(name = "Editor", goal = "Produce clean articles")
public class EditorRole extends Role implements Summarizer, Translator {
    // No capability bodies.
}
```

The first time `Summarizer` is used by another role, the duplication problem is already solved — update the prompt once, every adopter gets the change.

## When Not to Use Capabilities

- **`ActionType.LOCAL` methods** — these have real bodies that the framework invokes via reflection. They are not "framework-dispatched" and do not have the `return null;` problem. Leave them on the concrete role class.
- **Methods that declare an `ActionResult` parameter** — they are validated-out of capability interfaces by design (see above). Keep them on the role.
- **One-off methods used by a single role** with no prospect of reuse — extracting a capability interface for one consumer adds indirection without benefit. Capabilities shine when two or more roles share the same `@ActionSpec`.

## Related

- [Action System](action-system.md) — routing, `ActionType` enum, executor types.
- [Roles](roles.md) — role identity, responsibilities, lifecycle.

## Implementation References

- `com.tnsai.capabilities.Capability` — the marker annotation (`@Target(TYPE)`, `@Retention(RUNTIME)`).
- `com.tnsai.actions.Actions.dispatchedByFramework()` — helper returning `DispatchedByFrameworkException` (subtype of `UnsupportedOperationException`).
- `com.tnsai.actions.ActionDiscovery` — two-pass discovery: role class first, then the capability interface chain.
