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
| Method | Default | Required? |
|---|---|---|
.skillStore(SkillStore) | none | Required to enable skills. Wiring a store auto-upgrades policy from OFF to AUTO. |
.skillResolver(SkillResolver) | KeywordSkillResolver | Optional. 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) | 3 | Cap on candidate descriptions surfaced per turn. Bounds prompt overhead. |
AgentBuilder.build() requires the accountability trio (TNS-298).
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.identity.AgentPrincipal;
import com.tnsai.llm.LLMClient;
import com.tnsai.roles.Role;
import com.tnsai.skills.FileSystemSkillStore;
import com.tnsai.skills.SkillResolverPolicy;
import java.nio.file.Path;
public final class SkillsRegistrationBuilderExample {
private SkillsRegistrationBuilderExample() {}
public static Agent build(
LLMClient llm,
Role myRole,
AgentPrincipal principal,
LiabilitySink sink,
AuthorityScope scope
) {
return AgentBuilder.create()
.id("research-agent")
.llm(llm)
.role(myRole)
.skillStore(new FileSystemSkillStore(Path.of(".tnsai/skills")))
.skillResolverPolicy(SkillResolverPolicy.AUTO)
.maxActiveSkills(5)
.principal(principal)
.liabilitySink(sink)
.authorityScope(scope)
.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.mdScans 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 aToolCallAction.Guideaction 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
SkillStoreis 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
- Skills overview
- Skill format — frontmatter + substitution
- Hooks —
SkillActivationEventrides the same bus as other TnsAI events - Accountability — skill activations correlate with the resulting liability records via
runId
Skills
On-demand modular knowledge between role and tools. The framework's answer to: how do I keep multi-step procedures and domain knowledge out of the always-on system prompt without losing them when they're actually relevant?
SKILL.md format
A SKILL.md file is a YAML frontmatter block followed by a markdown body. The framework's parser (SkillMdParser) accepts the same shape Claude Code's parser does, so a skill authored once works in both environments.