---
slug: "skills-engine"
source_type: "readme"
source_url: "https://cdn.jsdelivr.net/gh/oalles/skills-engine@main/README.md"
repo: "https://github.com/oalles/skills-engine"
source_file: "README.md"
branch: "main"
---
# Skills Engine

This project is a practical proposal: treat **Skills** as first-class semantic and executable units — Spring components
that bundle **knowledge, interface, and governance**.

It grew out of a close study of
*[Recursive Advisors](https://spring.io/blog/2025/11/04/spring-ai-recursive-advisors)* and from dissecting the Spring AI
call chain (the advisor loop, the `ToolCallingManager` and the tool-calling plumbing). The repository is intentionally a
hands-on playground and reference implementation — pragmatic and exploratory, meant to illustrate how small, cohesive
knowledge modules can be authored, composed and governed for real systems rather than to present a finished,
one-size-fits-all framework.

## TL;DR

Skills as Spring components that can be injected, executed in workflows, packaged as Maven artifacts, and (optionally)
exposed to an LLM as part of the session context.

## Annotation-first model

Authoring a skill is as natural as `@Service`, but with semantic meaning.

- `@Skill` — marks a class as a skill (id, name, description, tags).
- `@SkillAttribute` — declares an attribute that will appear in a `SkillSnapshot`; can be `lazy` (evaluated with a
  timeout) and has a `minVisibilityLevel`.
- `@SkillTool` — exposes a method as a tool callback. Important options: `expose()` (`PSL` / `ALWAYS` / `DISABLED`),
  `minVisibilityLevel()`, and `returnDirect()`.

Compact example:

```java
@Skill(id = "helpdesk-advanced", name = "Helpdesk Advanced")
public class HelpdeskAdvancedSkill {

    @SkillAttribute(description = "Default SLA", minVisibilityLevel = SkillVisibilityLevel.METADATA)
    private final String defaultSla = "48h";

    @SkillAttribute(minVisibilityLevel = SkillVisibilityLevel.COMPLETE, lazy = true)
    public List<String> getRecentSlaExceptions() { ...}

    @SkillTool(name = "get_ticket_faq",
            minVisibilityLevel = SkillVisibilityLevel.CONTEXT,
            expose = SkillTool.ExposePolicy.PSL,
            returnDirect = true)
    public String getTicketFaq(String ticketId) { ...}
}
````

### Skills as components

A **Skill** is a cohesive semantic unit (knowledge + capability) within a bounded context. Because Skills are
implemented as Spring components they can be:

* Invoked directly in code (like any Spring bean)
* Injected into agents and workflows
* Made visible to the LLM when helpful
* Packaged and distributed as Maven artifacts

### SkillTools are Spring AI tools

**SkillTools** extend Spring AI tools: they are method-backed `ToolCallback`s that integrate with Spring’s method-based
tool providers/resolvers and the `ToolCallingManager`. Declaring a tool with `@SkillTool` gives it a Skill-scoped
identity (namespaced by `skillId`) and publishes it through the Skill-aware catalogs and resolvers the autoconfigure
installs.

What `@SkillTool` adds vs a plain `@Tool`:

* **Namespacing / metadata** — published as a `SkillToolRef` and scoped to the owning Skill.
* **PSL-aware exposure** — `expose` + `minVisibilityLevel` make the tool a *candidate to be PSL’ed* (hidden/revealed
  per-session). This is the main semantic difference.
* **Operational hints** — `returnDirect` and similar fields let the runtime optimize call/response behavior.
* **Integration** — Skill-aware providers/resolvers (`SkillAwareToolCallbackProvider`, `SkillAwareToolCallingManager`)
  inject only the PSL-permitted Skill tools into the runtime.

**Tiny example**

```java

@Skill(id = "helpdesk")
public class HelpdeskSkill {
    @SkillTool(name = "get_ticket_faq", expose = SkillTool.ExposePolicy.PSL,
            minVisibilityLevel = SkillVisibilityLevel.CONTEXT, returnDirect = true)
    public String getTicketFaq(String ticketId) { ...}
}
```

### SkillAttribute — runtime-resolved values (fields or methods)

A **SkillAttribute** is *read-only contextual state* exposed in the `SkillSnapshot`. Attributes may be declared as
simple fields **or** as no-arg methods (suppliers). We could use fields for cheap constants and annotate methods when
the value requires computation or I/O — the runtime treats method attributes as suppliers and evaluates them during
snapshot composition.

Key points:

* **Not every method is a tool.** SkillAttributes are *observations* (getters), tools are *actions*. Keeping this
  distinction
  keeps the callable surface smaller and safer.
* **Lazy & bounded.** Mark expensive attributes `lazy=true`; they run under a bounded timeout and become `null` on
  timeout (the snapshot still exposes the attribute schema). Configure timeouts and truncation via Boot properties.

**Micro example (traffic light)** — state vs action:

```java
@Skill(id = "traffic-control",
        name = "Traffic Control",
        description = """
                Manage and observe traffic lights for an intersection. 
                Exposes lightweight metadata (intersection id), 
                a lazy-observed current light state for snapshots, and a tool to 
                change switch position""")
public class TrafficControlSkill {

    // cheap metadata: intersection identifier (useful for discovery/audit)
    @SkillAttribute(description = "Intersection identifier (e.g. 'A-123')",
            minVisibilityLevel = SkillVisibilityLevel.METADATA)
    private final String intersectionId = "A-123";

    // State observation (attribute)
    @SkillAttribute(description = "Current traffic light state",
            minVisibilityLevel = SkillVisibilityLevel.CONTEXT,
            lazy = true)
    public String currentLightState() {
        return lightSensor.readState();
    }

    // State mutation (tool)
    @SkillTool(name = "change_switch",
            description = "Change the local switch position",
            expose = SkillTool.ExposePolicy.PSL,
            minVisibilityLevel = SkillVisibilityLevel.COMPLETE)
    public boolean changeSwitch(String switchId, String position) {
        return switchController.setPosition(switchId, position);
    }
}
```

The model sees `currentLightState` as a fact in the snapshot (no need to call a tool each turn to learn the light), and
calls `changeSwitch` only to mutate state.

## SkillSnapshot — the semantic cluster that conditions the LLM

When the runtime prepares a call to the LLM it builds a **`SkillSnapshot`** for every skill involved. A `SkillSnapshot`
is a compact, PSL-filtered, JSON-serializable representation of a Skill — its metadata, selected attributes, and the
descriptions of available tools — that the runtime **passes to the LLM as a semantic cluster** to condition reasoning.

**Key points**

* **Immutable per request.** A `SkillSnapshot` represents the view of a skill at a particular visibility level and is
  immutable for that model request. Snapshots are recomposed when session visibility changes so each LLM turn sees a
  consistent, auditable view.
* **What it contains.** Metadata (id, name, description, version, tags), the PSL-selected attributes (values or `null`
  if a lazy evaluation timed out), and the set of tool descriptions visible under the current PSL rules.
* **Lazy attributes & timeouts.** Attributes may be fields or no-arg methods (suppliers). Annotate methods when the
  value requires computation or I/O. Mark expensive attributes `lazy=true`: the provider evaluates them under a bounded
  timeout and writes `null` on timeout (the attribute schema is still present). Configure lazy timeouts and string
  truncation via the Boot properties (`spring.ai.skills.attributes.lazy-timeout-millis`,
  `spring.ai.skills.attributes.max-string-length`).
* **Audit & governance.** Snapshot creation and visibility changes are auditable; the project provides in-memory audit
  sinks and SPIs you can replace with persistent, cluster-safe stores.

**How it’s composed and injected**

* A `SkillSnapshotProvider` composes the JSON `SkillSnapshot` (the autoconfiguration registers
  `DefaultSkillSnapshotProvider` by default). You may implement your own `SkillSnapshotProvider` to change the JSON
  shape or composition rules.
* A `SkillSnapshotSystemPromptComposer` transforms snapshots into the *system prompt* string that conditions the model.
  The autoconfigure provides a `defaultComposer` but you can supply a custom composer to control formatting, redaction,
  or richer prompt engineering.
* `SkillAwareToolCallAdvisor` refreshes snapshots before each model call and injects the composed system prompt into the
  request so the LLM always receives the current PSL-filtered semantic cluster. This advisor is the runtime hook that
  ensures snapshots actually participate in the chat loop.

**Why this matters**
A `SkillSnapshot` prevents ad-hoc dumping of domain data into prompts. It treats Skills as **semantic clusters**
—bounded, governed packages of knowledge that the LLM can rely on, reason with, or explicitly ask to expand via PSL
control tools.

## PSL — Progressive Skill Loading (a simple engine)

**PSL** is the canonical visibility model: think of Skill content as **drawers** you open or close per-session. PSL
controls *how much* of a Skill (attributes and tools) is visible to the LLM at runtime.

**Core ideas**

* **Visibility levels:** `METADATA` → `CONTEXT` → `COMPLETE`. `METADATA` is the baseline visibility level; attributes
  and
  tools with `minVisibilityLevel=METADATA` are eligible at this level. `CONTEXT` and `COMPLETE` open additional
  attributes and tools.
* **ExposePolicy + minVisibilityLevel.** `@SkillTool` and `@SkillAttribute` declare `expose` (an `ExposePolicy`) and a
  `minVisibilityLevel`. `expose=PSL` makes an element’s visibility depend on session-level PSL state; `expose=ALWAYS`
  makes it globally visible regardless of PSL. This is the primary semantic difference that turns `@SkillTool` entries
  into *candidates to be PSL’ed*.

**PSL as an operable Skill**

* PSL control is itself a Skill (`PslSkill`). The engine exposes control tools —
  `setSkillVisibility(sessionId, skillId, level)`, `getVisibility(sessionId)`, and `clearVisibility(sessionId)` — as
  methods on that Skill. Those control tools are published (the implementation exposes them with `ExposePolicy.ALWAYS`)
  so an authorized orchestrator or the model (when allowed) can mutate visibility safely. That design intentionally
  makes the LLM an **operator of its own context** while preserving governance.

**Snapshot + tool discovery integration**

* When PSL changes, snapshots are regenerated so each model turn has a consistent view. The Skill-aware components (
  `SkillAwareToolCallbackProvider`, `SkillAwareToolCallingManager`) consult `PslService` to determine which Skill tool
  callbacks are visible for the session and inject only those into the tool discovery/resolution process. This enforces
  PSL both in the prompt and at tool-discovery time.

**Operational notes & best practices**

* **PSL-first posture.** Prefer `expose=PSL` and conservative `minVisibilityLevel` (e.g., start at `CONTEXT` and
  escalate to `COMPLETE` only when necessary).
* **Guard PSL control tools.** Restrict `PslSkill` methods to orchestrator or trusted roles and audit visibility
  changes. (The default `PslSkill` is powerful and should be carefully protected.)
* **Stores & production readiness.** The autoconfigure registers in-memory visibility and audit stores by default (
  non-persistent, non-cluster-safe). Replace them with cluster-safe implementations for production by providing beans
  implementing the respective SPIs (`SkillVisibilityStore`, `SkillSnapshotAuditStore`, `SkillVisibilityAuditStore`).

### Visibility lifecycle and ephemeral visibilities (operational note)

**Principle.** PSL visibility is session state: **do not clear it automatically at the end of each model call**.
Visibility normally persists until an orchestrator or the `PslSkill` explicitly clears it, or until the session expires.
Clearing visibility indiscriminately after each call, doFinalizeLoop, breaks continuity across turns and undermines
workflows that expect persistent context.

**When to revoke visibility**

* **Explicit removal:** callers should use `pslSkill.clearVisibility(sessionId)` or `setVisibility(..., null)` to remove
  visibility intentionally.
* **Ephemeral (one-shot) visibility:** support a flag (e.g. `ephemeral=true` or `ttlMillis`) on `setVisibility` for
  temporary elevations. These entries must be revoked automatically after the unit of work completes (end of turn /
  request).
* **TTL / housekeeping:** implement expiry in the `SkillVisibilityStore` (store timestamps and purge stale entries) to
  recover state from abandoned sessions.
* **Where to revoke ephemeral visibility**
  The **ToolCallAdvisor finalizer** (the advisor hook that runs after the model/tool loop completes) is the natural
  place
  to revoke ephemeral visibilities for the session. In this project the appropriate hook is the advisor’s
  finalize/after-call method (for the supplied advisor it is best placed in the advisor’s finalize hook — e.g.
  `doFinalizeLoop` / `doAfterCall` equivalent). The advisor already sees the `SessionId` and manages the turn lifecycle,
  so it can call `pslService.revokeEphemeralVisibilitiesForSession(sessionId)` in a `finally` block to guarantee
  cleanup.

**Example (conceptual)**

```text
# The PSL control tools available on the 'psl-skill':
psl-skill.setSkillVisibility(sessionId, "billing-skill", SkillVisibilityLevel.COMPLETE)
psl-skill.getVisibility(sessionId)
psl-skill.clearVisibility(sessionId)
```

These operations are exposed as a Skill so the LLM (or a host orchestrator) can request additional context in a
controlled, auditable way.

## Quickstart — the full example

**Run the full example:**

```bash
mvn -f examples/full-example/full-example-agent/pom.xml spring-boot:run
```

**Ask the agent:**

```bash
curl -X POST http://localhost:8080/ask/s1 \
  -H "Content-Type: text/plain" \
  -d "Check system health and deploy 'payments-service' if OK."
```

**Inspect session state:**

```bash
curl http://localhost:8080/visibility/s1
curl http://localhost:8080/snapshot/s1
```

Look at logs for:

* `SkillAwareToolCallAdvisor` — snapshot recomposition.
* `PslSkill` — visibility changes.
* Skill-specific logs when tools run.

![Full Example](https://github.com/oalles/skills-engine/raw/HEAD/full-example.gif)

## Packaging Skills as plugins

Skills can be packaged as standalone jars with auto-configuration:

**Producer (Skill jar):**

* Annotate Skill classes (`@Skill`)
* Provide Boot `@AutoConfiguration`
* Include auto-configuration metadata (`spring.factories` or `AutoConfiguration.imports`)

**Consumer (app):**

* Depend on Skill jar and include `skills-engine-boot-starter`
* Boot auto-configuration registers the Skill bean
* Runtime toggles: enable/disable the Skills engine or scanning via `spring.ai.skills.*` properties

## Practical advantages

* Reduced token usage and clearer context composition.
* Reusable, testable components that teams can publish and share.
* Built-in governance: per-session visibility, audit trails, and control tooling.
* Flexible deployment: skills can power the agent workflow and/or be made available to the LLM.

## Disclaimer

This repository is a technical **demo** that presents the Skills concept and a simple Progressive Skill
Loading (PSL) engine 😉 Just that!


