Module: Mistri::Skills
- Defined in:
- lib/mistri/skills.rb
Overview
Loads skills and wires them into an agent: their one-line descriptions ride the system prompt, and the model pulls a full body on demand with the read_skill tool — so a large library costs almost nothing until a skill is actually used.
Class Method Summary collapse
- .amend(system, skills) ⇒ Object
-
.load(path) ⇒ Object
Reads a directory of skills in either layout:
/ /SKILL.md or / .md. -
.parse(text) ⇒ Object
Frontmatter is deliberately a subset: flat string keys between --- markers, quotes optional.
- .read(file, default_name:) ⇒ Object
- .reader(skills) ⇒ Object
-
.section(skills) ⇒ Object
The always-present cost of a skill library: one line per skill.
Class Method Details
.amend(system, skills) ⇒ Object
48 49 50 51 52 |
# File 'lib/mistri/skills.rb', line 48 def amend(system, skills) return system if skills.empty? [system, section(skills)].compact.join("\n\n") end |
.load(path) ⇒ Object
Reads a directory of skills in either layout:
14 15 16 17 18 19 20 21 22 23 24 25 26 |
# File 'lib/mistri/skills.rb', line 14 def load(path) raise ConfigurationError, "no skills directory at #{path}" unless File.directory?(path) skills = Dir.children(path).sort.filter_map do |child| full = File.join(path, child) if File.directory?(full) && File.file?(File.join(full, "SKILL.md")) read(File.join(full, "SKILL.md"), default_name: child) elsif child.end_with?(".md") && File.file?(full) read(full, default_name: File.basename(child, ".md")) end end skills.sort_by(&:name) end |
.parse(text) ⇒ Object
Frontmatter is deliberately a subset: flat string keys between --- markers, quotes optional. name and description are the whole contract, and a YAML dependency is not worth two fields.
68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/mistri/skills.rb', line 68 def parse(text) return [{}, text] unless text.start_with?("---\n") head, separator, body = text[4..].partition("\n---\n") return [{}, text] if separator.empty? = {} head.scan(/^([a-z_]+):[ \t]*(.+?)[ \t]*$/) do |key, value| [key] = value.gsub(/\A["']|["']\z/, "") end [, body.sub(/\A\n+/, "")] end |
.read(file, default_name:) ⇒ Object
28 29 30 31 32 |
# File 'lib/mistri/skills.rb', line 28 def read(file, default_name:) , body = parse(File.read(file)) Skill.new(name: .fetch("name", default_name), description: .fetch("description", ""), body: body) end |
.reader(skills) ⇒ Object
54 55 56 57 58 59 60 61 62 63 |
# File 'lib/mistri/skills.rb', line 54 def reader(skills) by_name = skills.to_h { |skill| [skill.name, skill] } Tool.define("read_skill", "Reads the full playbook for a named skill.", schema: -> { string :name, "Skill name", required: true }) do |args| skill = by_name[args["name"]] next skill.body if skill "Unknown skill #{args["name"].inspect}. Available: #{by_name.keys.join(", ")}" end end |
.section(skills) ⇒ Object
The always-present cost of a skill library: one line per skill.
35 36 37 38 39 40 41 42 43 44 45 46 |
# File 'lib/mistri/skills.rb', line 35 def section(skills) lines = skills.map { |skill| "- #{skill.name}: #{skill.description}" } <<~TEXT.strip ## Skills Expert playbooks. Before acting, check this list: when a skill matches the task, you MUST call read_skill with its name and follow the playbook. #{lines.join("\n")} TEXT end |