Class: Mistri::Definition

Inherits:
Object
  • Object
show all
Defined in:
lib/mistri/definition.rb

Overview

An agent definition: a markdown file whose YAML frontmatter carries the config and whose body is the prompt. Prompts stay editable prose in files a reviewer can diff; code stays out of them.

---
role: Trip Planner
model: claude-opus-4-8
tools:
- search_flights
- book_hotel
---
You plan trips end to end. Address the traveler as {first_name}.

definition = Mistri::Definition.load("app/agents/trip_planner.md")
agent = Mistri.agent(definition.model,
                   system: definition.render(first_name: traveler.first_name),
                   tools: registry.build(definition.tools, traveler))

The gem reads the file; what tool names mean, and what any extra frontmatter keys mean, stays the host's vocabulary via #config.

Constant Summary collapse

FRONTMATTER =
/\A---\s*\n(?<yaml>.*?)\n---\s*\n(?<body>.*)\z/m

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name:, config:, body:) ⇒ Definition

Returns a new instance of Definition.



41
42
43
44
45
46
# File 'lib/mistri/definition.rb', line 41

def initialize(name:, config:, body:)
  @name = name.to_s
  @config = config.freeze
  @body = body
  freeze
end

Instance Attribute Details

#bodyObject (readonly)

Returns the value of attribute body.



29
30
31
# File 'lib/mistri/definition.rb', line 29

def body
  @body
end

#configObject (readonly)

Returns the value of attribute config.



29
30
31
# File 'lib/mistri/definition.rb', line 29

def config
  @config
end

#nameObject (readonly)

Returns the value of attribute name.



29
30
31
# File 'lib/mistri/definition.rb', line 29

def name
  @name
end

Class Method Details

.load(path) ⇒ Object

Raises:



31
32
33
34
35
36
37
38
39
# File 'lib/mistri/definition.rb', line 31

def self.load(path)
  match = FRONTMATTER.match(File.read(path))
  raise ConfigurationError, "#{path} has no frontmatter" unless match

  config = YAML.safe_load(match[:yaml]) || {}
  raise ConfigurationError, "#{path} frontmatter is not a mapping" unless config.is_a?(Hash)

  new(name: File.basename(path.to_s, ".md"), config: config, body: match[:body].strip)
end

Instance Method Details

#modelObject



48
# File 'lib/mistri/definition.rb', line 48

def model = config["model"]

#render(vars = {}) ⇒ Object

The body with placeholders filled in. An unfilled placeholder raises: a prompt silently addressing "first_name" is worse than an error. A value of nil renders as empty, which lets optional context collapse cleanly.



71
72
73
74
75
76
77
78
# File 'lib/mistri/definition.rb', line 71

def render(vars = {})
  vars = vars.transform_keys(&:to_s)
  body.gsub(/\{(\w+)\}/) do
    vars.fetch(Regexp.last_match(1)) do |key|
      raise ConfigurationError, "no value for {#{key}} in the #{name} definition"
    end
  end
end

#roleObject



50
# File 'lib/mistri/definition.rb', line 50

def role = config["role"]

#tool_namesObject



65
# File 'lib/mistri/definition.rb', line 65

def tool_names = tools.keys

#toolsObject

Tool declarations as a name => options map. A bare list means no options; a map form carries per-tool options in the host's vocabulary (gates, caching, whatever the host honors).



55
56
57
58
59
60
61
62
63
# File 'lib/mistri/definition.rb', line 55

def tools
  raw = config["tools"]
  entries = case raw
            when Hash then raw.transform_values { |options| options || {} }
            when Array then raw.to_h { |tool| [tool, {}] }
            else {}
            end
  entries.transform_keys(&:to_s)
end