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.
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.
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 aSkillSnapshot; can belazy(evaluated with a timeout) and has aminVisibilityLevel.@SkillTool— exposes a method as a tool callback. Important options:expose()(PSL/ALWAYS/DISABLED),minVisibilityLevel(), andreturnDirect().
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) { ...}
}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 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
SkillToolRefand scoped to the owning Skill. - PSL-aware exposure —
expose+minVisibilityLevelmake the tool a candidate to be PSL’ed (hidden/revealed per-session). This is the main semantic difference. - Operational hints —
returnDirectand 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) { ...}
}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 becomenullon 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.
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
SkillSnapshotrepresents 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
nullif 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 writesnullon 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
SkillSnapshotProvidercomposes the JSONSkillSnapshot(the autoconfiguration registersDefaultSkillSnapshotProviderby default). You may implement your ownSkillSnapshotProviderto change the JSON shape or composition rules. - A
SkillSnapshotSystemPromptComposertransforms snapshots into the system prompt string that conditions the model. The autoconfigure provides adefaultComposerbut you can supply a custom composer to control formatting, redaction, or richer prompt engineering. SkillAwareToolCallAdvisorrefreshes 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 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.METADATAis the baseline visibility level; attributes and tools withminVisibilityLevel=METADATAare eligible at this level.CONTEXTandCOMPLETEopen additional attributes and tools. - ExposePolicy + minVisibilityLevel.
@SkillTooland@SkillAttributedeclareexpose(anExposePolicy) and aminVisibilityLevel.expose=PSLmakes an element’s visibility depend on session-level PSL state;expose=ALWAYSmakes it globally visible regardless of PSL. This is the primary semantic difference that turns@SkillToolentries 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), andclearVisibility(sessionId)— as methods on that Skill. Those control tools are published (the implementation exposes them withExposePolicy.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) consultPslServiceto 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=PSLand conservativeminVisibilityLevel(e.g., start atCONTEXTand escalate toCOMPLETEonly when necessary). - Guard PSL control tools. Restrict
PslSkillmethods to orchestrator or trusted roles and audit visibility changes. (The defaultPslSkillis 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).
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)orsetVisibility(..., null)to remove visibility intentionally. - Ephemeral (one-shot) visibility: support a flag (e.g.
ephemeral=trueorttlMillis) onsetVisibilityfor 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/doAfterCallequivalent). The advisor already sees theSessionIdand manages the turn lifecycle, so it can callpslService.revokeEphemeralVisibilitiesForSession(sessionId)in afinallyblock 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.
Run the full example:
mvn -f examples/full-example/full-example-agent/pom.xml spring-boot:runAsk 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/s1Look at logs for:
SkillAwareToolCallAdvisor— snapshot recomposition.PslSkill— visibility changes.- Skill-specific logs when tools run.
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.factoriesorAutoConfiguration.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
- 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.
This repository is a technical demo that presents the Skills concept and a simple Progressive Skill Loading (PSL) engine 😉 Just that!
