Class: Omakase::Agent

Inherits:
Object
  • Object
show all
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
MAX_DEPTH =

Each level opens a chat with a fresh tool budget, so depth is what bounds the cost. ponytail: a constant; a setting when a real tree needs more.

10

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(chat: nil) ⇒ Agent

chat: injects a prepared RubyLLM::Chat — the seam for tests.



140
141
142
# File 'lib/omakase/agent.rb', line 140

def initialize(chat: nil)
  @chat = chat
end

Class Method Details

.chat_options ⇒ Object



94
# File 'lib/omakase/agent.rb', line 94

def chat_options = @chat_options ||= {}

.describe(text) ⇒ Object

Documents the method defined next — the docstring Ruby does not have.



61
62
63
# File 'lib/omakase/agent.rb', line 61

def describe(text)
  @pending_description = text
end

.descriptions ⇒ Object



92
# File 'lib/omakase/agent.rb', line 92

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.



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/omakase/agent.rb', line 68

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



90
# File 'lib/omakase/agent.rb', line 90

def generations = @generations ||= {}

.instructions(text = nil) ⇒ Object



23
24
25
26
27
# File 'lib/omakase/agent.rb', line 23

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", …}. The server opens on the first instance, not at class load.



38
# File 'lib/omakase/agent.rb', line 38

def mcp(name, **options) = MCP.defer(self, name, options)

.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.



53
54
55
56
57
58
# File 'lib/omakase/agent.rb', line 53

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.



16
17
18
19
20
21
# File 'lib/omakase/agent.rb', line 16

def model(id = nil, **options)
  return chat_options if id.nil? && options.empty?

  options = {assume_model_exists: true, **options} if options[:provider]
  @chat_options = {model: id, **options}.compact
end

.new ⇒ Object

Here rather than in initialize, which a subclass may override and never call super from — and then generate with none of its tools.



42
43
44
45
# File 'lib/omakase/agent.rb', line 42

def new(...)
  MCP.ensure(self)
  super
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.



49
# File 'lib/omakase/agent.rb', line 49

def skill(path) = Skills.attach(self, path)

.strategy(name = nil) ⇒ Object



29
30
31
32
33
# File 'lib/omakase/agent.rb', line 29

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.



147
# File 'lib/omakase/agent.rb', line 147

def chat(**overrides) = @chat || Omakase.chat_factory.call(**self.class.chat_options.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.



151
# File 'lib/omakase/agent.rb', line 151

def context = nil

#doc(object) ⇒ Object

For generated code meeting an object whose type it does not know.



160
# File 'lib/omakase/agent.rb', line 160

def doc(object) = puts(Doc.of(object))

#finish(value) ⇒ Object

How generated code answers: with the value itself.



163
# File 'lib/omakase/agent.rb', line 163

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.



155
# File 'lib/omakase/agent.rb', line 155

def marshal_dump = (instance_variables - [:@chat]).to_h { |name| [name, instance_variable_get(name)] }

#marshal_load(state) ⇒ Object



157
# File 'lib/omakase/agent.rb', line 157

def marshal_load(state) = state.each { |name, value| instance_variable_set(name, value) }

#p(*args) ⇒ Object Also known as: pp



171
172
173
174
# File 'lib/omakase/agent.rb', line 171

def p(*args)
  args.each { |arg| omakase_output.puts(arg.inspect) }
  (args.size <= 1) ? args.first : args
end


169
# File 'lib/omakase/agent.rb', line 169

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.



167
# File 'lib/omakase/agent.rb', line 167

def puts(*args) = omakase_output.puts(*args)