Class: Omakase::Agent
- Inherits:
-
Object
- Object
- Omakase::Agent
- Defined in:
- lib/omakase/agent.rb
Overview
Fields are state, methods are what the model can call, generates declares
the methods the model implements.
Constant Summary collapse
- RUNNING =
The generations this thread is inside, so one cannot re-enter itself.
:omakase_running
Class Method Summary collapse
- .chat_options ⇒ Object
-
.describe(text) ⇒ Object
Documents the method defined next — the docstring Ruby does not have.
- .descriptions ⇒ Object
-
.generates(name, prompt = nil, takes: nil, returns: nil, strategy: nil, model: nil, &schema) ⇒ Object
Without a prompt, the method name is the prompt.
- .generations ⇒ Object
- .instructions(text = nil) ⇒ Object
-
.mcp(name, **options) ⇒ Object
An MCP server's tools, as methods on the agent.
-
.memory ⇒ Object
Two more methods: one to save something, one to search it by meaning.
-
.model(id = nil, **options) ⇒ Object
The model and any RubyLLM chat option.
-
.skill(path) ⇒ Object
A skill directory — a SKILL.md with YAML front matter.
- .strategy(name = nil) ⇒ Object
Instance Method Summary collapse
-
#chat(**overrides) ⇒ Object
A fresh conversation per call — two threads calling one agent must not share a mutable chat.
-
#context ⇒ Object
That state, as the model should read it: rebuilt on every call, and added to the class's instructions.
-
#doc(object) ⇒ Object
For generated code meeting an object whose type it does not know.
-
#finish(value) ⇒ Object
How generated code answers: with the value itself.
-
#initialize(chat: nil) ⇒ Agent
constructor
chat:injects a prepared RubyLLM::Chat — the seam for tests. -
#marshal_dump ⇒ Object
Resuming a run is loading the object back, so an agent marshals like any other Ruby object — minus the live chat, which is rebuilt on demand.
- #marshal_load(state) ⇒ Object
- #p(*args) ⇒ Object (also: #pp)
- #print(*args) ⇒ Object
-
#puts(*args) ⇒ Object
Printing from generated code goes to the observation, not to the process's stdout — and the buffer is per thread, so concurrent agents stay separate.
Constructor Details
#initialize(chat: nil) ⇒ Agent
chat: injects a prepared RubyLLM::Chat — the seam for tests.
131 132 133 |
# File 'lib/omakase/agent.rb', line 131 def initialize(chat: nil) @chat = chat end |
Class Method Details
.chat_options ⇒ Object
86 |
# File 'lib/omakase/agent.rb', line 86 def = @chat_options ||= {} |
.describe(text) ⇒ Object
Documents the method defined next — the docstring Ruby does not have.
53 54 55 |
# File 'lib/omakase/agent.rb', line 53 def describe(text) @pending_description = text end |
.descriptions ⇒ Object
84 |
# File 'lib/omakase/agent.rb', line 84 def descriptions = @descriptions ||= {} |
.generates(name, prompt = nil, takes: nil, returns: nil, strategy: nil, model: nil, &schema) ⇒ Object
Without a prompt, the method name is the prompt. A block instead of a
string is a prompt read at call time, on the agent. takes: names the
keyword arguments, and then Ruby checks them.
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/omakase/agent.rb', line 60 def generates(name, prompt = nil, takes: nil, returns: nil, strategy: nil, model: nil, &schema) # Redeclaring an inherited generation is how a subclass specialises one. # Landing on a method you wrote is not that, and would replace it unseen. if Capabilities.names(self).include?(name) && !generations.key?(name) raise Error, "#{self}##{name} is already a method — generates would replace it" end unless prompt.nil? || prompt.is_a?(String) || prompt.is_a?(Proc) raise Error, "#{self}##{name}: a prompt is a String or a block returning one, got #{prompt.class}" end generations[name] = Generation.new( name:, prompt: prompt || humanize(name), schema: Schema.define(returns:, &schema), strategy: Strategies.fetch(strategy || self.strategy), model: ) define_generation_method(name, takes) define_singleton_method(name) { |**inputs| new.public_send(name, **inputs) } end |
.generations ⇒ Object
82 |
# File 'lib/omakase/agent.rb', line 82 def generations = @generations ||= {} |
.instructions(text = nil) ⇒ Object
20 21 22 23 24 |
# File 'lib/omakase/agent.rb', line 20 def instructions(text = nil) return @instructions.to_s if text.nil? @instructions = text end |
.mcp(name, **options) ⇒ Object
An MCP server's tools, as methods on the agent. Options are passed to
ruby_llm-mcp verbatim: mcp :files, transport_type: :stdio, config: {command: "npx", …}.
34 35 36 37 |
# File 'lib/omakase/agent.rb', line 34 def mcp(name, **) require "ruby_llm/mcp" MCP.attach(self, RubyLLM::MCP.add_client(name: name.to_s, **)) end |
.memory ⇒ Object
Two more methods: one to save something, one to search it by meaning. The store is a field, so it marshals with the agent and outlives the run.
45 46 47 48 49 50 |
# File 'lib/omakase/agent.rb', line 45 def memory describe "Save something worth remembering after this run" define_method(:remember) { |text| (@memory ||= Memory.new).remember(text) } describe "Search what you remember, by meaning; the closest few come back" define_method(:recall) { |query, limit: 5| (@memory ||= Memory.new).recall(query, limit:) } end |
.model(id = nil, **options) ⇒ Object
The model and any RubyLLM chat option. Naming a provider takes the model id on trust, since providers like OpenRouter or Ollama serve ids that are not in RubyLLM's registry.
13 14 15 16 17 18 |
# File 'lib/omakase/agent.rb', line 13 def model(id = nil, **) return if id.nil? && .empty? = {assume_model_exists: true, **} if [:provider] @chat_options = {model: id, **}.compact end |
.skill(path) ⇒ Object
A skill directory — a SKILL.md with YAML front matter. Its description joins the agent's capabilities; its body arrives when the model asks.
41 |
# File 'lib/omakase/agent.rb', line 41 def skill(path) = Skills.attach(self, path) |
.strategy(name = nil) ⇒ Object
26 27 28 29 30 |
# File 'lib/omakase/agent.rb', line 26 def strategy(name = nil) return @strategy || :code_act if name.nil? @strategy = name end |
Instance Method Details
#chat(**overrides) ⇒ Object
A fresh conversation per call — two threads calling one agent must not share a mutable chat. What carries between calls is the object's own state. Overrides land on top of the class's options; an injected chat ignores them.
138 |
# File 'lib/omakase/agent.rb', line 138 def chat(**overrides) = @chat || Omakase.chat_factory.call(**self.class..merge(overrides)) |
#context ⇒ Object
That state, as the model should read it: rebuilt on every call, and added to the class's instructions. Override it to remember anything.
142 |
# File 'lib/omakase/agent.rb', line 142 def context = nil |
#doc(object) ⇒ Object
For generated code meeting an object whose type it does not know.
151 |
# File 'lib/omakase/agent.rb', line 151 def doc(object) = puts(Doc.of(object)) |
#finish(value) ⇒ Object
How generated code answers: with the value itself.
154 |
# File 'lib/omakase/agent.rb', line 154 def finish(value) = throw(Executor::RESULT, value) |
#marshal_dump ⇒ Object
Resuming a run is loading the object back, so an agent marshals like any other Ruby object — minus the live chat, which is rebuilt on demand.
146 |
# File 'lib/omakase/agent.rb', line 146 def marshal_dump = (instance_variables - [:@chat]).to_h { |name| [name, instance_variable_get(name)] } |
#marshal_load(state) ⇒ Object
148 |
# File 'lib/omakase/agent.rb', line 148 def marshal_load(state) = state.each { |name, value| instance_variable_set(name, value) } |
#p(*args) ⇒ Object Also known as: pp
162 163 164 165 |
# File 'lib/omakase/agent.rb', line 162 def p(*args) args.each { |arg| omakase_output.puts(arg.inspect) } (args.size <= 1) ? args.first : args end |
#print(*args) ⇒ Object
160 |
# File 'lib/omakase/agent.rb', line 160 def print(*args) = omakase_output.print(*args) |
#puts(*args) ⇒ Object
Printing from generated code goes to the observation, not to the process's stdout — and the buffer is per thread, so concurrent agents stay separate.
158 |
# File 'lib/omakase/agent.rb', line 158 def puts(*args) = omakase_output.puts(*args) |