Module: Pikuri::Skill::Renderer
- Defined in:
- lib/pikuri/skill/renderer.rb
Overview
Turns Catalog::Skill data into the Agent Skills XML the model actually reads. Three renders, all stateless:
-
Renderer.format_catalog — the
<available_skills>inventory for the system prompt (advertises what's loadable):<available_skills> <skill> <name>pdf-tools</name> <description>Extracts text from PDFs.</description> </skill> </available_skills> -
Renderer.format_promotion — the same inventory shape, injected mid-loop when a touched file brings gated skills into play. Carries
<when-to-use>where the catalog block doesn't, since a promotion is a pitch for one moment rather than a resident line item.
Neither inventory render carries the skill's path, only Renderer.format_skill
does — which the standard sanctions outright ("if your dedicated
activation tool provides the skill directory path in its result, you can
omit location from the catalog"). An inventory entry is a routing
decision, and a path is unusable until the body names what to read; on a
28-skill catalog the duplicate ran ~400 resident tokens.
-
Renderer.format_skill — one skill's body block, injected when the skill is loaded (by the
skilltool or a host-driven load):Sidecar paths in this skill (e.g. scripts/, references/) are relative to /repo/.pikuri/skills/pdf-tools.
Presentation lives here, not on Catalog (which stays pure discovery / validation / holding) nor on SkillTool (a callable action): both renders have callers in different classes whose only shared handle is the catalog data, so a stateless module reached with that data fits both without handle-passing.
Class Method Summary collapse
-
.format_catalog(skills) ⇒ String
The
<available_skills>inventory block, or""for an empty list (so callers concatenate unconditionally). -
.format_promotion(skills) ⇒ String
The mid-loop promotion block: skills a touched file just brought into play, in the same shape the catalog block uses and carrying the same instruction, so the model reads it the way it reads the resident one.
-
.format_skill(skill) ⇒ String
One skill's
<skill>body block: the wrapper carriesnameandlocationso the model resolves relative sidecar paths against the skill's directory via thereadtool.
Class Method Details
.format_catalog(skills) ⇒ String
The <available_skills> inventory block, or "" for an empty list (so
callers concatenate unconditionally). Leads with two blank lines;
callers joining prompt sections lstrip as needed.
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/pikuri/skill/renderer.rb', line 54 def format_catalog(skills) return '' if skills.empty? lines = [ '', '', 'The following skills provide specialized instructions for specific tasks.', "When a skill matches the task, load it with the `skill` tool *before* answering from your own knowledge — the skill's instructions take precedence over your training. If none matches, just proceed normally.", '', '<available_skills>' ] skills.each { |skill| lines.concat(entry_lines(skill, trigger: false)) } lines << '</available_skills>' lines.join("\n") end |
.format_promotion(skills) ⇒ String
The mid-loop promotion block: skills a touched file just brought into play, in the same shape the catalog block uses and carrying the same instruction, so the model reads it the way it reads the resident one.
The trailing instruction is not decoration. Announced as mere
availability, a promotion loses to whatever the model already believes
is a sufficient answer — observed on a local Qwen3.6, which named the
promoted skill in its own reasoning, resolved to load it, then read a
neighbouring source file instead and imitated its style. Imitation
transmits presence and never absence, so every rule of the form
"delete this" was lost. Hence the precedence clause names inferred
house style alongside training, and the if none matches escape rides
with it so a coarse glob's false positive is cheap to decline.
87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/pikuri/skill/renderer.rb', line 87 def format_promotion(skills) lines = [ 'These skills just became relevant to the files you are working with, and are now loadable:', '', '<available_skills>' ] skills.each { |skill| lines.concat(entry_lines(skill, trigger: true)) } lines << '</available_skills>' lines << '' lines << 'If one matches what you are about to do, load it with the `skill` tool first — its ' \ 'instructions take precedence over your training and over any convention you infer ' \ 'from the surrounding code. If none matches, just proceed normally.' lines.join("\n") end |
.format_skill(skill) ⇒ String
One skill's <skill> body block: the wrapper carries name and
location so the model resolves relative sidecar paths against the
skill's directory via the read tool.
108 109 110 111 112 113 114 115 116 117 |
# File 'lib/pikuri/skill/renderer.rb', line 108 def format_skill(skill) base_dir = File.dirname(skill.location) <<~OUT <skill name="#{escape_xml(skill.name)}" location="#{escape_xml(skill.location)}"> Sidecar paths in this skill (e.g. scripts/, references/) are relative to #{base_dir}. #{skill.body} </skill> OUT end |