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