Agent definitions

An agent definition is a markdown file. The YAML frontmatter carries the config (model, tools, anything else you want), the body is the prompt, and {placeholders} fill in when you build the agent. The prompt stays editable prose a reviewer can diff, and code stays out of it.

app/agents/trip_planner.md
---
role: Trip Planner
model: claude-opus-4-8
tools:
- search_flights
- book_hotel
---
You plan trips end to end. Keep the itinerary tight and realistic,
and address the traveler as {first_name}.

Loading and rendering

Definition.load reads the file; render fills the placeholders and returns the finished prompt.

definition = Mistri::Definition.load("app/agents/trip_planner.md")
definition.model # => "claude-opus-4-8"
definition.role # => "Trip Planner"
definition.tool_names # => ["search_flights", "book_hotel"]
agent = Mistri.agent(
definition.model,
system: definition.render(first_name: traveler.first_name),
tools: registry.build(definition.tools, traveler),
)

The gem reads the file and nothing more. What a tool name means, and what any extra frontmatter key means, stays your vocabulary. Turning the declared names into real, per-user tools is the registry recipe.

Placeholders fail loudly

An unfilled placeholder raises rather than shipping a prompt that addresses the traveler as the literal string {first_name}:

definition.render
# => Mistri::ConfigurationError: no value for {first_name} in the trip_planner definition

A value of nil renders as empty, so optional context collapses cleanly instead of forcing a branch on the caller:

# body: "Plan the trip.{scope_note}"
definition.render(scope_note: nil) # => "Plan the trip."

Tools carry options

A bare list declares tool names with no options. The map form attaches per-tool options in your vocabulary, which your registry reads however it likes (a gate, a cache setting, a flag):

---
tools:
book_hotel:
gate: manager-approval
search_flights:
---
definition.tools
# => { "book_hotel" => { "gate" => "manager-approval" }, "search_flights" => {} }

A tool entry with no options (like search_flights above) comes back as an empty hash, so definition.tools is always a name-to-options map you can iterate without a nil check.

Extra keys ride through

Any frontmatter key you add is available on config, so a definition can carry host settings the gem never interprets:

# frontmatter: itinerary_style: relaxed
definition.config["itinerary_style"] # => "relaxed"

Why files

A definition is prose in version control. Product can rewrite the prompt in a pull request a reviewer diffs like any other change, the model and tool list sit right beside it, and no deploy rewrites a string constant buried in Ruby. A file without frontmatter raises on load, so a half-written definition fails at boot rather than mid-run.

For the identity a definition’s tools act under at runtime, see App context.