A Spectre.Skill packages reusable conversation behavior that an Agent can
mount. It can own flows, route handlers, prompt injections, policies, and
action hooks, while the Agent continues to own the runtime infrastructure and
the real side-effect boundary.
Use a Skill when the same capability should be shared by multiple Agents, or when a large Agent is easier to understand as several independently scoped capabilities. Keep behavior that is specific to one small Agent directly in that Agent.
Starting with Spectre 0.2.7, trusted hosts may also construct data-only Skills
through Spectre.Skill.Definition and manage them with
Spectre.Skill.Runtime. They lower into the same canonical model, use only
registered operation references and closed prompt fragments, and have a
separate authority-checked lifecycle. See
Runtime Skills and Routing Projections. The compiled mount
path described below remains unchanged.
The Smallest Complete Example
Define the Skill with use Spectre.Skill:
defmodule MyApp.Skills.Greeting do
use Spectre.Skill, id: :greeting, version: 1
flow :greeting do
on :HELLO, regex: ~r/^hello$/i do
run(:greet)
end
end
def greet(%Spectre.Input{text: text}, _ctx) do
{:ok, "The greeting skill received: #{text}"}
end
endMount it in an Agent with skill/2:
defmodule MyApp.Agent do
use Spectre.Agent
skill(MyApp.Skills.Greeting, as: :greeting)
endUse the Agent normally. Spectre includes mounted Skill routes during routing and invokes the callback on the module that owns the selected route:
{:ok, result} = Spectre.ask(MyApp.Agent, "hello")
result.reply_text
#=> "The greeting skill received: hello"
result.route.scope
#=> {:skill, :greeting}as: :greeting is the local mount identifier. It becomes the route, policy,
effect, and prompt scope, so two mounted Skills can reuse the same flow names
and route labels without losing ownership.
If as: is omitted, the Skill's id: is used. Mount identifiers must be
unique within an Agent.
Binding Actions
A reusable Skill should not know the concrete action names used by every host application. Declare a logical requirement in the Skill, use that logical name in its handlers, and bind it when mounting the Skill.
defmodule MyApp.Skills.DocumentSearch do
use Spectre.Skill, id: :document_search, version: 1
requires_action(:search, mode: :read)
flow :document_search do
on :SEARCH_DOCUMENTS, regex: ~r/\bsearch (the )?docs\b/i do
action(:search, args: %{index: "help-center"})
end
end
endThe Agent owns the concrete action module and binds :search to an action in
that module:
defmodule MyApp.Actions do
def search_help_center(args, ctx) do
MyApp.Search.run(args.index, ctx.input.text)
end
end
defmodule MyApp.SupportAgent do
use Spectre.Agent
actions(MyApp.Actions)
skill(MyApp.Skills.DocumentSearch,
as: :docs,
bind: [search: :search_help_center]
)
endThe logical action is materialized as the concrete action while retaining its Skill owner and scope:
{:ok, turn} = Spectre.turn(MyApp.SupportAgent, "search the docs")
{:needs,
%Spectre.Effect{
name: :search_help_center,
mode: :read,
scope: {:skill, :docs},
status: :pending
} = effect, result} = turn.decision
{:ok, completed} = Spectre.execute(MyApp.SupportAgent, result)mode: may be :read, :write, or :destructive. Every declared requirement
must have a binding, and a Skill cannot stage, protect, or attach hooks to an
action that it did not declare. These rules are checked while the Agent
compiles.
requires_tool/2 is an alias for requires_action/2 when tool terminology is
more natural for the capability.
Skill Policies
Policies may live with the reusable behavior. Protect the logical action name; Spectre applies the policy to the bound concrete action and keeps the policy in the mount's scope.
defmodule MyApp.Skills.Publisher do
use Spectre.Skill,
id: :publisher,
version: 1,
prompt_root: "priv/skills/publisher/prompts"
requires_action(:publish, mode: :write)
policy :confirm_publish do
request(:confirm_publish)
accept(:accepted, regex: ~r/^yes$/i)
reject(:rejected, regex: ~r/^no$/i)
end
protect(:publish, with: :confirm_publish)
flow :publishing do
on :PUBLISH, regex: ~r/^publish$/i do
action(:publish)
end
end
endMount it just like the previous example:
skill(MyApp.Skills.Publisher,
as: :publisher,
bind: [publish: :publish_article]
)When :PUBLISH routes, the resulting effect is named :publish_article, its
status is :waiting_policy, and its open awaitable is named
{{:skill, :publisher}, :confirm_publish}. Approval still only changes state;
the host must explicitly call Spectre.execute/3 afterward. If the Agent also
protects the bound concrete action, the Agent's protection takes precedence.
Prompts And Injections
A Skill may set its own prompt_root: and use the same reason, reply, and
inject declarations as an Agent:
defmodule MyApp.Skills.Answers do
use Spectre.Skill,
id: :answers,
version: 1,
prompt_root: "priv/skills/answers/prompts"
inject(:answering_rules, into: :instructions, position: :end)
flow :answers do
on :ANSWER, regex: ~r/^answer:/i do
reason(:answer)
end
end
endFor a selected Skill route, Agent-level injections are composed with that
Skill's injections. Prompt names declared by the Skill resolve below its own
prompt root. In this example, reason(:answer) resolves:
priv/skills/answers/prompts/answer.text.heexThe model used for reason still comes from the mounting Agent.
Private Generational State
Starting with Spectre 0.2.5, a stable Skill id can own private state as a
canonical Spectre.Skill.StateBinding. The Instance—not the Skill DSL—is the
single sequencer for this state. Updates require the selected branch's exact
schema Ref, generation, and revision, and are fenced by the active Definition
and current Instance owner lease.
Definition changes create explicit state history. In an A → B → A sequence, B's branch is never merged into A. If the target Definition already has a dormant branch, activation must explicitly resume, fork, migrate, or abandon it. The common stateless case and a target with no dormant branch need no choice.
Use Spectre.skill_state/3, Spectre.skill_state_branches/3,
Spectre.update_skill_state/4, and
Spectre.transition_skill_state_retention/5 at the trusted host boundary.
See Generational Skill State for activation examples,
retention rules, and checkpoint compatibility.
What A Skill Inherits
A Skill describes behavior; it does not create a session or establish a second runtime. The mounting Agent supplies:
- the resolved Stack and capability bindings;
- the router and arbitrator;
- model, classifier, and embedding adapters;
- the action module;
- state, memory, and journal adapters;
- the input pipeline, history, failure behavior, and session lifecycle.
Consequently, do not declare model, classifier, embedding, router,
actions, state, memory, journal, input_pipeline, history, idle,
shutdown, fail, or stack inside a Skill. Invalid infrastructure
declarations are rejected at compile time.
A Skill cannot mount another Skill. Compose multiple Skills at the Agent level:
defmodule MyApp.Assistant do
use Spectre.Agent
skill(MyApp.Skills.Greeting, as: :greeting)
skill(MyApp.Skills.DocumentSearch,
as: :docs,
bind: [search: :search_help_center]
)
endThe Agent's configured routing strategies decide among Agent-owned and
Skill-owned routes together. Route receipts expose scope, making the selected
owner explicit for logging, persistence, and tests.
Checklist
To add a Skill:
- Create a module with
use Spectre.Skill, id: ..., version: 1. - Add flows, handlers, prompts, policies, injections, or hooks with the normal Spectre DSL.
- Declare every logical action with
requires_action/2. - Mount it in an Agent with
skill SkillModule, as: .... - Bind each required action with
bind: [logical_name: :concrete_name]. - Call
Spectre.ask/3orSpectre.turn/3on the Agent, not on the Skill.
See also DSL, Actions, and the Spectre.Skill
moduledoc in lib/spectre/skill.ex.