Skip to content

oalles/skills-engine

Repository files navigation

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

@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 ToolCallbacks 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 exposureexpose + minVisibilityLevel make the tool a candidate to be PSL’ed (hidden/revealed per-session). This is the main semantic difference.
  • Operational hintsreturnDirect 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

@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:

@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: METADATACONTEXTCOMPLETE. 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)

# 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:

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

Ask the agent:

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:

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

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!

About

Skills Engine — a Spring-native MVP that models Skills as semantic components (metadata, attributes, tools) and provides a simple Progressive Skill Loading (PSL) engine to condition and govern LLMs.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages