TnsAI
CapabilitiesSkills

Registration

How to wire a SkillStore and SkillResolver into an AgentBuilder, swap the defaults, and integrate skill activation with the rest of the framework.

AgentBuilder API

MethodDefaultRequired?
.skillStore(SkillStore)noneRequired to enable skills. Wiring a store auto-upgrades policy from OFF to AUTO.
.skillResolver(SkillResolver)KeywordSkillResolverOptional. Override only if the default token-overlap scoring isn't accurate enough.
.skillResolverPolicy(SkillResolverPolicy)OFF (or AUTO once a store is wired)Override to force MANUAL_ONLY (resolver does not run) or OFF (skill layer disabled even when a store is present).
.maxActiveSkills(int)3Cap on candidate descriptions surfaced per turn. Bounds prompt overhead.
Agent agent = AgentBuilder.create()
        .id("research-agent")
        .llm(llm)
        .role(myRole)
        .skillStore(new FileSystemSkillStore(Path.of(".tnsai/skills")))
        .skillResolverPolicy(SkillResolverPolicy.AUTO)
        .maxActiveSkills(5)
        // accountability wiring (TNS-298) elided
        .build();

When skillStore is not called, agent.getSkillManager() returns null and the agent operates without a skills layer — the documented "skills disabled" contract.

Stores

FileSystemSkillStore

The recommended production store. Reads the Claude Code-compatible layout:

.tnsai/skills/
├── deploy/SKILL.md
└── lint/SKILL.md

Scans on construction. Re-scan via refresh() to pick up disk changes:

FileSystemSkillStore store = new FileSystemSkillStore(Path.of(".tnsai/skills"));
// ... time passes, someone added a new skill ...
store.refresh();

Programmatic registrations (store.register(skill)) survive refresh() if their name doesn't collide with a disk skill. Disk content takes precedence on collision.

InMemorySkillStore

Test seam. Programmatic-only:

InMemorySkillStore store = new InMemorySkillStore(List.of(
        Skill.builder("deploy")
                .description("Production deploy procedure")
                .body("1. Verify CI...\n2. kubectl apply...")
                .allowedTools(List.of("bash", "kubectl"))
                .build()));

Custom stores

Implement SkillStore to back skills with a database / classpath / enterprise registry. The contract is small (findByName, list, register); concurrency must be safe under multi-threaded agent dispatch.

Resolvers

KeywordSkillResolver (default)

Cheap word-overlap scoring. Field weights: name 3×, when-to-use 2×, description 1×. Stop-words filtered. Zero-score candidates dropped. Ties break alphabetically by name for stable ordering.

LLMSkillResolver

Asks the configured LLMClient to pick the most relevant skill names from a compact catalog. Higher accuracy when triggers are paraphrased semantically; one extra LLM call per turn.

.skillResolver(new LLMSkillResolver(llmClient))

Falls back to KeywordSkillResolver on LLM failure rather than silently returning empty — a transient hiccup must not strip skills from the prompt.

Custom resolvers

Implement SkillResolver. Common variants:

  • Embedding-based — pre-compute description embeddings; score by cosine similarity against the user message embedding.
  • Hybrid — keyword + LLM rerank.
  • Rule-based — wire-up that always surfaces a fixed subset.

Tool-call scope (SkillScopedToolCallFilter)

When skills declare allowed-tools, attach the skill-aware filter so the LLM is constrained to those tools while the skill is active:

agent.setToolCallFilter(new SkillScopedToolCallFilter(agent.getSkillManager()));

Behaviour summary:

  • No active skills → permissive (every tool call allowed).
  • Active skills, none declare allowed-tools → permissive.
  • One or more active skills declare allowed-tools → tool calls outside the union of their lists are blocked with a ToolCallAction.Guide action that names the active skills, so the LLM can self-correct.

Compose with other filters by wrapping in your own ToolCallFilter chain.

Subagent context fork

When an AgentGroup spawns a subagent that should inherit the parent's procedural context, preload from the parent's manager:

SkillManager parentMgr = parent.getSkillManager();
SkillManager childMgr = child.getSkillManager();
if (parentMgr != null && childMgr != null) {
    childMgr.preloadFrom(parentMgr);
}

The child's pre-existing activations are preserved on top; the parent's snapshots are appended. Re-activations replace any same-named child snapshot.

Programmatic invocation

Agent.invokeSkill(name, arguments, environment) activates a skill directly. The flag userInvocable=false is bypassed (programmatic source overrides user-facing visibility), but disableModelInvocation is irrelevant here because the source is PROGRAMMATIC, not MODEL_TOOL_CALL.

Optional<ActiveSkill> activated = agent.invokeSkill(
        "deploy",
        List.of("staging"),
        Map.of("BUILD_TAG", "v1.4.2"));

Returns empty when:

  • No SkillStore is wired.
  • No skill with the given name is registered.

Slash commands

Users (or test harnesses) activate a skill manually with /skill-name args.... Agent.chat(...) intercepts this prefix BEFORE the LLM round-trip, so manual invocation costs zero LLM tokens and returns a confirmation string.

String reply = agent.chat("/deploy staging");
// "Skill 'deploy' activated; its body is now in context and will guide subsequent turns."

userInvocable=false skills reject slash invocation with a friendly error instead of activating.

Activation events

Every successful activation emits a SkillActivationEvent (sealed branch of TnsAIEvent):

public record SkillActivationEvent(
        String eventId,
        Instant timestamp,
        String runId,
        String agentName,
        String skillName,
        Source source,                // USER_SLASH_COMMAND / MODEL_TOOL_CALL / PROGRAMMATIC
        List<String> arguments
) implements TnsAIEvent {}

Use agent.chatWithEvents(message, eventConsumer) to receive these alongside the rest of the event stream. The same event flows through the configured hook bus, so policy hooks (e.g. "log every skill activation to the audit pipeline") can attach there.

See also

On this page