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

<!-- java-contract: src/main/java/com/example/tnsai/docs/SkillsRegistrationBuilderExample.java -->
```java
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.md
```

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

```java
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:

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

```java
.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:

```java
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:

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

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

```java
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`):

```java
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](index.md)
- [Skill format](skill-format.md) — frontmatter + substitution
- [Hooks](../../security/index.md) — `SkillActivationEvent` rides the same bus as other TnsAI events
- [Accountability](../../security/accountability.md) — skill activations correlate with the resulting liability records via `runId`
