Skip to content
tnsaiDocs
GitHub

Capabilities

A capability is a reusable action with no body. You declare @ActionSpec methods once on a @Capability interface, and every role that implements the interface gains those actions. The framework dispatches each call, so no role writes a method body.

Use a capability for LLM, MCP_TOOL and WEB_SERVICE actions that two or more roles share. For those types the framework never runs the method body when the method has no ActionResult parameter, so a concrete return null; body is dead code. To move existing return null; methods onto capabilities, see From return null; bodies to @Capability.

Declare and adopt a capability

The interface method is default, and its body throws Actions.dispatchedByFramework(). That body runs only if someone calls the method directly instead of through the agent, and then it throws DispatchedByFrameworkException rather than returning a silent null.

package com.example.tnsai.docs;

import com.tnsai.actions.Actions;
import com.tnsai.annotations.ActionSpec;
import com.tnsai.capabilities.Capability;
import com.tnsai.enums.ActionType;
import com.tnsai.models.role.RoleIdentity;
import com.tnsai.roles.Role;

public final class CapabilitiesExample {
    private CapabilitiesExample() {}

    @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();
        }
    }

    @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();
        }
    }

    // Two capabilities, no action bodies on the role.
    public static final class AssistantRole extends Role implements Summarizer, Translator {
        @Override
        public RoleIdentity getIdentity() {
            return new RoleIdentity("assistant", "Handle ad-hoc requests", "general");
        }
    }

    // The role's own method replaces the capability's LLM action with a LOCAL one.
    public static final class StrictEditorRole extends Role implements Summarizer {
        @Override
        public RoleIdentity getIdentity() {
            return new RoleIdentity("strict-editor", "Shorten text deterministically", "editing");
        }

        @Override
        @ActionSpec(type = ActionType.LOCAL, description = "Truncate the text to 60 characters")
        public String summarize(String text) {
            return text.length() <= 60 ? text : text.substring(0, 60) + "...";
        }
    }
}
  • AssistantRole gets two LLM actions, summarize and translate, from its interfaces.
  • Discovery walks the role's interfaces, its superclasses and the interfaces those extend, and collects every one annotated with @Capability.
  • StrictEditorRole declares summarize itself. Discovery records the role's own signatures first and skips a capability default with the same signature, so the role has one summarize action and its type is LOCAL.

Rules checked at discovery

Discovery throws IllegalStateException naming the interface and method when a capability method breaks either rule.

  • It must be default. An abstract method would force every role to write a body:

    @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.
  • It must not take an ActionResult parameter. Post-processing of the raw result belongs on a concrete method of the role:

    @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.

A role can mix capability actions and its own methods freely.

When not to use one

  • LOCAL actions. Their body is the action, so keep them on the role.
  • Actions that post-process an ActionResult. The validator rejects them.
  • An action only one role uses. An interface with one implementer adds indirection and nothing else.

Capabilities, @RoleDeclaration and @RoleSpec

These answer different questions, and a role can carry all three.

  • A capability adds actions.
  • @RoleDeclaration holds identity metadata (name, goal, domain, description, version) that role exporters read. It does not replace getIdentity(), which every Role subclass still implements.
  • @RoleSpec configures the role: its model (llm), knowledge, goals and config. See Roles.