TnsAI
CapabilitiesSkills

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?

The com.tnsai.skills package in tnsai-core ships the primitives:

  • Skill — record carrying a name, a description (the trigger phrase the resolver scores against), the markdown body, and per-skill scoping (allowedTools, userInvocable, disableModelInvocation).
  • SkillStore — SPI for discovery; the framework ships InMemorySkillStore (test seam) and FileSystemSkillStore (Claude Code-compatible <root>/<skill>/SKILL.md layout).
  • SkillResolver — picks top-N candidates per user turn; defaults to KeywordSkillResolver (no extra round-trip), upgradeable to LLMSkillResolver (semantic match, one extra LLM call per turn).
  • SkillManager — per-agent facade combining store + resolver + session state. Handles /skill-name parsing, invocation source gating, and prompt-section rendering.
  • SkillSession + ActiveSkill — runtime state tracking which skills the agent has activated this session.
  • SkillScopedToolCallFilterToolCallFilter that enforces the union of allowed-tools across active skills.

Skills sit between the always-on role layer and the always-on tool layer:

LayerLoadedGranularity
Role / CLAUDE.mdAlwaysStable agent identity
ToolAlwaysAtomic action
CapabilityAlways (compile-time)Method-level declaration
RAG hitPer query (similarity)Evidence text
SkillOn demand (intent-matched)Multi-step procedure
HookAlways (system gate)Cross-cutting policy

This is the "Skills" layer in Claude Code's 5-layer architecture (CLAUDE.md / Skills / Hooks / Subagents / Plugins). TnsAI was missing it before TNS-289.

Why a separate layer

Three forces motivate skills as a distinct primitive:

  1. Token budget hygieneRoleSpec and CLAUDE.md content rides every prompt. Procedures that only matter when invoked ("deploy procedure", "API design conventions", "customer escalation protocol") shouldn't pin tokens on every turn.
  2. Author-time portability — Claude Code, Cursor, and other tools are converging on the agentskills.io SKILL.md standard. A skill authored once works across every framework that supports it.
  3. Per-skill scopingallowed-tools declares which tools a skill expects to use; userInvocable and disableModelInvocation declare who may activate it. Both are recorded on the activation event and enforceable through SkillScopedToolCallFilter.

Quick start

import com.tnsai.skills.*;
import com.tnsai.agents.AgentBuilder;
import java.nio.file.Path;

// 1. Pick a store. FileSystemSkillStore reads the Claude Code-compatible
//    layout: <root>/<skill-name>/SKILL.md. Programmatic registration
//    works through InMemorySkillStore for tests / embedded use.
SkillStore store = new FileSystemSkillStore(Path.of(".tnsai/skills"));

// 2. Wire on the AgentBuilder. Wiring a store auto-upgrades the
//    policy from OFF to AUTO (the consumer's intent: "I configured
//    skills, surface them"). Override with .skillResolverPolicy(...)
//    if you want MANUAL_ONLY or OFF.
Agent agent = AgentBuilder.create()
        .id("research-agent")
        .llm(...)
        .role(myRole)
        .skillStore(store)
        // accountability wiring (TNS-298) elided for brevity
        .build();

From this point on:

  • The resolver runs on every chat turn against the registered skill descriptions; the top maxActiveSkills candidates appear in the per-message system prompt under ## Skill candidates for this turn.
  • The user types /deploy staging to manually activate a skill — the framework intercepts BEFORE the LLM round-trip, so manual invocation costs zero LLM tokens.
  • Once activated, the skill's substituted body lives in the system prompt under # Skills > ## Active skill bodies for the remainder of the session.
  • agent.invokeSkill("name", args, env) lets framework code activate a skill programmatically, bypassing userInvocable=false.

SKILL.md format

---
name: deploy
description: Production deploy procedure with rollback support
when-to-use: When the user asks to ship a build to prod or staging
allowed-tools:
  - bash
  - kubectl
argument-hint: <environment>
arguments:
  - environment
disable-model-invocation: false
user-invocable: true
---
# Deploy procedure

1. Verify CI is green.
2. `kubectl apply -f manifests/$0/`

The $0 placeholder gets substituted with the first positional argument when the skill is invoked. See Skill format for the full frontmatter reference.

Resolver policies

PolicyBehaviour
AUTO (default when a store is wired)Resolver runs on every user turn; top-N candidate descriptions appear in the system prompt; the LLM may invoke via the synthetic invoke_skill tool; users may invoke via /skill-name.
MANUAL_ONLYResolver does NOT run; only /skill-name and agent.invokeSkill(...) activate skills. Useful when the deployment wants deterministic skill loading.
OFFSkill layer disabled. Framework default when no store is wired.

Lifecycle

  1. Discovery — store enumerates registered skills at startup. Only the description field is in the always-on system prompt.
  2. Resolution — per user message, SkillResolver ranks candidates by relevance.
  3. Activation — user invocation or model tool call moves the full body into context for the rest of the session.
  4. Re-activation — invoking an active skill again replaces the previous snapshot; the latest invocation's substituted body wins.

SkillActivationEvent (sealed branch of TnsAIEvent) is emitted on every activation, carrying the source (USER_SLASH_COMMAND / MODEL_TOOL_CALL / PROGRAMMATIC), the skill name, and the supplied arguments.

Per-skill tool scope

When a skill declares allowed-tools, callers can attach SkillScopedToolCallFilter to the agent so tool calls outside the union of active-skill allowed-tools are blocked with a Guide action that names the active skills:

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

The filter is permissive when no active skill declares allowed-tools — the field is opt-in scoping, not a default constraint.

Subagent context fork

SkillManager.preloadFrom(parent) copies the parent's active-skill snapshots into a child manager so AgentGroup-spawned subagents inherit the procedural context their parent had loaded. Mirrors Claude Code's context: fork pattern. Parent's snapshot wins on name collision; the child's pre-existing activations are preserved on top.

What's not in this layer (deferred)

  • paths glob auto-activation — file-context-aware activation; v2 (the v1 trigger surface is user-message-aware via the resolver)
  • Plugin distribution — packaging skills into the Plugins layer; tracked separately
  • context: fork semantics for full isolation — current preload is a copy, not a fork; v2
  • GUI skill registry — visual catalog in TnsAI.Web; v3
  • Live file-watcher — call FileSystemSkillStore.refresh() instead

See also

  • Skill format — full SKILL.md frontmatter reference and substitution rules
  • RegistrationAgentBuilder API + custom resolvers + custom stores
  • Hooks — skill activation events flow through the hook bus
  • Approvals and Annotations@ApprovalRequired works alongside skills (approvals gate access; skills supply the procedure)
  • AccountabilitySkillActivationEvent rides the same trace as the resulting liability records

On this page