omakase-agents 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +83 -0
- data/README.md +252 -23
- data/lib/omakase/agent.rb +80 -13
- data/lib/omakase/capabilities.rb +7 -2
- data/lib/omakase/doc.rb +20 -4
- data/lib/omakase/executor.rb +152 -11
- data/lib/omakase/fake_chat.rb +28 -3
- data/lib/omakase/mcp.rb +90 -1
- data/lib/omakase/request.rb +15 -3
- data/lib/omakase/schema.rb +56 -6
- data/lib/omakase/skills/how_to_act/SKILL.md +40 -0
- data/lib/omakase/skills.rb +18 -4
- data/lib/omakase/strategies/code_act.rb +26 -9
- data/lib/omakase/strategies/predict.rb +8 -2
- data/lib/omakase/tools/ruby.rb +12 -4
- data/lib/omakase/trace.rb +50 -0
- data/lib/omakase/type.rb +15 -2
- data/lib/omakase/version.rb +1 -1
- data/lib/omakase.rb +33 -1
- metadata +6 -4
data/lib/omakase/schema.rb
CHANGED
|
@@ -43,9 +43,10 @@ module Omakase
|
|
|
43
43
|
|
|
44
44
|
# From the provider's JSON: unwrap first, then hold it to the contract.
|
|
45
45
|
def cast(content)
|
|
46
|
+
content = parse(content) if content.is_a?(String)
|
|
46
47
|
raise ContractError, "expected JSON matching #{JSON.generate(json)}, got #{content.inspect}" unless content.is_a?(Hash)
|
|
47
48
|
|
|
48
|
-
data =
|
|
49
|
+
data = symbolize(content)
|
|
49
50
|
take(wrapped? ? data.fetch(RESULT) { raise ContractError, %(missing "result" in #{data.inspect}) } : data)
|
|
50
51
|
end
|
|
51
52
|
|
|
@@ -54,9 +55,9 @@ module Omakase
|
|
|
54
55
|
return demand(value, properties.fetch("result")["type"]) if wrapped?
|
|
55
56
|
raise ContractError, "expected #{describe}, got #{value.inspect}" unless value.is_a?(Hash)
|
|
56
57
|
|
|
57
|
-
data =
|
|
58
|
-
|
|
59
|
-
raise ContractError, "
|
|
58
|
+
data = symbolize(value)
|
|
59
|
+
problem = object_mismatch(data, json, nil)
|
|
60
|
+
raise ContractError, "#{problem} — expected #{describe}" if problem
|
|
60
61
|
|
|
61
62
|
data
|
|
62
63
|
end
|
|
@@ -67,11 +68,60 @@ module Omakase
|
|
|
67
68
|
|
|
68
69
|
def properties = json.fetch("properties")
|
|
69
70
|
|
|
71
|
+
# RubyLLM 2 hands structured output back as a JSON string. Not JSON stays a
|
|
72
|
+
# String, so cast reports what the model actually said.
|
|
73
|
+
def parse(content)
|
|
74
|
+
JSON.parse(content)
|
|
75
|
+
rescue JSON::ParserError
|
|
76
|
+
content
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def symbolize(value)
|
|
80
|
+
case value
|
|
81
|
+
when Hash then value.to_h { |key, item| [key.respond_to?(:to_sym) ? key.to_sym : key, symbolize(item)] }
|
|
82
|
+
when Array then value.map { |item| symbolize(item) }
|
|
83
|
+
else value
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
70
87
|
def demand(value, type)
|
|
71
|
-
|
|
72
|
-
raise ContractError, "expected <#{type}>, got #{value.inspect}" unless matched
|
|
88
|
+
raise ContractError, "expected <#{type}>, got #{value.inspect}" unless type?(value, type)
|
|
73
89
|
|
|
74
90
|
value
|
|
75
91
|
end
|
|
92
|
+
|
|
93
|
+
def type?(value, type)
|
|
94
|
+
case type
|
|
95
|
+
when "boolean" then [true, false].include?(value)
|
|
96
|
+
when "null" then value.nil?
|
|
97
|
+
when Array then type.any? { |each| type?(value, each) }
|
|
98
|
+
else value.is_a?(RUBY_TYPES.fetch(type, BasicObject))
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# The first place a nested value breaks its schema, as a path the model can fix.
|
|
103
|
+
def mismatch(value, spec, path)
|
|
104
|
+
return "#{path}: expected <#{Array(spec["type"]).join("|")}>, got #{value.inspect}" if spec["type"] && !type?(value, spec["type"])
|
|
105
|
+
return "#{path}: expected one of #{spec["enum"].inspect}, got #{value.inspect}" if spec["enum"] && !spec["enum"].include?(value)
|
|
106
|
+
|
|
107
|
+
case value
|
|
108
|
+
when Array then value.each_with_index.filter_map { |item, i| mismatch(item, spec["items"], "#{path}[#{i}]") if spec["items"] }.first
|
|
109
|
+
when Hash then object_mismatch(value, spec, path)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# An optional field left nil is a field left out, which is allowed.
|
|
114
|
+
def object_mismatch(value, spec, path)
|
|
115
|
+
required = Array(spec["required"])
|
|
116
|
+
missing = required.map(&:to_sym) - value.keys
|
|
117
|
+
return [path, "missing #{missing.join(", ")}"].compact.join(": ") if missing.any?
|
|
118
|
+
|
|
119
|
+
Hash(spec["properties"]).filter_map do |name, child|
|
|
120
|
+
field = value[name.to_sym]
|
|
121
|
+
next if field.nil? && !required.include?(name)
|
|
122
|
+
|
|
123
|
+
mismatch(field, child, [path, name].compact.join(".")) if value.key?(name.to_sym)
|
|
124
|
+
end.first
|
|
125
|
+
end
|
|
76
126
|
end
|
|
77
127
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: how-to-act
|
|
3
|
+
description: How to write the Ruby that implements a generation — finish, prints, doc, ivars. Call once before you act.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You write Ruby. It runs on the agent: its methods and ivars are yours, on self.
|
|
7
|
+
|
|
8
|
+
Call a method. Print what you need to see. `finish` with the answer — the value, not a sentence about it.
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
orders = orders_for("ada@example.com")
|
|
12
|
+
orders.each { |o| puts "total: #{o.total}" }
|
|
13
|
+
puts policy_on(:damage)
|
|
14
|
+
finish(Refund.new(order_id: 1, amount: 39.9, reason: "cracked mug, policy :damage"))
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Without `finish`, the last expression comes back as `=> …` and you keep going.
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
stock_of("apple") + stock_of("pear")
|
|
21
|
+
# => 7
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The inputs are local variables. Locals and ivars last for the rest of this generation:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
n = items.sum { |item| stock_of(item) }
|
|
28
|
+
# later:
|
|
29
|
+
finish(n + 1)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
An object you do not know:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
doc(orders.first) # a class works too
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
For meaning — classify, judge, summarize — call a generation method, not a regex. Never type out large data by hand: compute it or take it from an input.
|
|
39
|
+
|
|
40
|
+
Prints come back to you, not to the process. If `finish` is refused, the message says why — fix it in the next call. Do not retype a value you already computed. Work in as few calls as you can.
|
data/lib/omakase/skills.rb
CHANGED
|
@@ -6,11 +6,18 @@ module Omakase
|
|
|
6
6
|
# capabilities; the body only arrives when the model calls the method — which
|
|
7
7
|
# is all "loaded on demand" has to mean.
|
|
8
8
|
module Skills
|
|
9
|
+
CORE = File.expand_path("skills/how_to_act", __dir__)
|
|
10
|
+
|
|
9
11
|
module_function
|
|
10
12
|
|
|
13
|
+
# Every agent gets the how-to. Skip when a parent already defined it.
|
|
14
|
+
def attach_core(agent_class)
|
|
15
|
+
attach(agent_class, CORE) unless Capabilities.names(agent_class).include?(:how_to_act)
|
|
16
|
+
end
|
|
17
|
+
|
|
11
18
|
def attach(agent_class, path)
|
|
12
19
|
directory = File.expand_path(path)
|
|
13
|
-
front_matter, body = parse(File.read(File.join(directory, "SKILL.md")))
|
|
20
|
+
front_matter, body = parse(File.read(File.join(directory, "SKILL.md"), encoding: "UTF-8"))
|
|
14
21
|
name = (front_matter["name"] || File.basename(directory)).tr("-", "_").to_sym
|
|
15
22
|
raise Error, "#{agent_class} already has ##{name}" if Capabilities.names(agent_class).include?(name)
|
|
16
23
|
|
|
@@ -19,12 +26,19 @@ module Omakase
|
|
|
19
26
|
name
|
|
20
27
|
end
|
|
21
28
|
|
|
22
|
-
# The front matter every SKILL.md in the wild is written with.
|
|
29
|
+
# The front matter every SKILL.md in the wild is written with — CRLF too.
|
|
23
30
|
def parse(text)
|
|
24
|
-
match = text.match(/\A---\n(.*?)\n---\n(.*)\z/m)
|
|
31
|
+
match = text.match(/\A---\r?\n(.*?)\r?\n---\r?\n(.*)\z/m)
|
|
25
32
|
return [{}, text.strip] unless match
|
|
26
33
|
|
|
27
|
-
[
|
|
34
|
+
[front_matter(match[1]), match[2].strip]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Claude Code style hints like `argument-hint: "<x>" [-p]` are not YAML; read those line by line.
|
|
38
|
+
def front_matter(text)
|
|
39
|
+
YAML.safe_load(text)
|
|
40
|
+
rescue Psych::SyntaxError
|
|
41
|
+
text.scan(/^([\w-]+):[ \t]*(.*?)\r?$/).to_h
|
|
28
42
|
end
|
|
29
43
|
end
|
|
30
44
|
end
|
|
@@ -9,14 +9,16 @@ module Omakase
|
|
|
9
9
|
|
|
10
10
|
def call(request)
|
|
11
11
|
tool = Tools::Ruby.new(request.agent, request.schema)
|
|
12
|
-
|
|
13
|
-
.
|
|
14
|
-
.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
chat = Predict.instruct(request.chat, instructions(request), request.context)
|
|
13
|
+
.with_tools(tool)
|
|
14
|
+
.ask_later(request.task(preview: true), with: request.attachments)
|
|
15
|
+
response = run(chat, tool)
|
|
16
|
+
# A text reply is usually a model that forgot how to answer, not one that is done.
|
|
17
|
+
response = run(chat.ask_later(nudge(request)), tool) unless tool.done?
|
|
18
18
|
return tool.answer.value if tool.answer
|
|
19
19
|
|
|
20
|
+
notes = response&.content
|
|
21
|
+
|
|
20
22
|
# It never called finish. A JSON answer can still be given in a tool-free turn.
|
|
21
23
|
return Predict.call(request, task: "#{request.task}\n\nWork done:\n#{notes}") unless request.schema.code_only?
|
|
22
24
|
|
|
@@ -24,18 +26,33 @@ module Omakase
|
|
|
24
26
|
raise ContractError, "#{request.generation.name}: the model never called finish(#{request.schema.describe})"
|
|
25
27
|
end
|
|
26
28
|
|
|
29
|
+
def run(chat, tool)
|
|
30
|
+
response = nil
|
|
31
|
+
response = chat.step until chat.complete? || tool.done?
|
|
32
|
+
response
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def nudge(request)
|
|
36
|
+
"Your reply was text with no tool call, so the task is not done. " \
|
|
37
|
+
"Call the `ruby` tool and end with finish(#{request.schema.describe})."
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Nothing here changes between calls of one generation, so providers can cache it.
|
|
27
41
|
def instructions(request)
|
|
28
42
|
<<~TEXT
|
|
29
43
|
#{request.instructions}
|
|
30
44
|
|
|
31
45
|
You act by writing Ruby: call the `ruby` tool with code that is evaluated on the
|
|
32
|
-
agent object, so its methods and state are available on self.
|
|
46
|
+
agent object, so its methods and state are available on self. The inputs are
|
|
47
|
+
local variables in that code — use them, do not retype them.
|
|
33
48
|
|
|
34
49
|
#{capabilities(request).join("\n")}
|
|
35
50
|
|
|
36
|
-
`doc(object)` prints what an object of an unfamiliar type offers.
|
|
51
|
+
`doc(object)` prints what an object of an unfamiliar type offers; a class works too.
|
|
52
|
+
`how_to_act` is the rest, with examples — call it once before you write code.
|
|
37
53
|
|
|
38
|
-
|
|
54
|
+
Run the task; do not define a method for it. Return the answer from inside the
|
|
55
|
+
code, never as a message — the last thing you run is:
|
|
39
56
|
|
|
40
57
|
finish(#{request.schema.describe})
|
|
41
58
|
|
|
@@ -9,8 +9,7 @@ module Omakase
|
|
|
9
9
|
module_function
|
|
10
10
|
|
|
11
11
|
def call(request, task: request.task)
|
|
12
|
-
chat = request.chat
|
|
13
|
-
.with_instructions(instructions(request))
|
|
12
|
+
chat = instruct(request.chat, instructions(request), request.context)
|
|
14
13
|
.with_schema(request.schema.definition)
|
|
15
14
|
|
|
16
15
|
request.schema.cast(chat.ask(task, with: request.attachments).content)
|
|
@@ -19,6 +18,13 @@ module Omakase
|
|
|
19
18
|
request.schema.cast(chat.ask("#{e.message}\n\n#{CORRECTION}").content)
|
|
20
19
|
end
|
|
21
20
|
|
|
21
|
+
# The stable text first and marked for the cache; the context, which
|
|
22
|
+
# changes from call to call, after it.
|
|
23
|
+
def instruct(chat, stable, context)
|
|
24
|
+
chat = chat.with_instructions(stable, cache_until_here: true)
|
|
25
|
+
context.empty? ? chat : chat.with_instructions(context, append: true)
|
|
26
|
+
end
|
|
27
|
+
|
|
22
28
|
# Weaker providers treat the schema as a hint, so it goes in the prompt too.
|
|
23
29
|
def instructions(request)
|
|
24
30
|
"#{request.instructions}\n\nAnswer as JSON matching this schema:\n#{JSON.generate(request.schema.json)}"
|
data/lib/omakase/tools/ruby.rb
CHANGED
|
@@ -6,6 +6,8 @@ module Omakase
|
|
|
6
6
|
# through it, so the model composes calls in code instead of one per turn.
|
|
7
7
|
class Ruby < RubyLLM::Tool
|
|
8
8
|
BUDGET = 10
|
|
9
|
+
# Only a fence around the whole code: one inside it is part of a string.
|
|
10
|
+
FENCE = /\A```(?:ruby|rb)?[ \t]*\r?\n(.*?)\r?\n?```\z/m
|
|
9
11
|
|
|
10
12
|
description <<~TEXT
|
|
11
13
|
Evaluate Ruby in the context of the agent object: its methods and state are
|
|
@@ -13,7 +15,7 @@ module Omakase
|
|
|
13
15
|
expression, is returned to you. Call finish(value) to answer.
|
|
14
16
|
TEXT
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
parameter :code, description: "Ruby source to evaluate."
|
|
17
19
|
|
|
18
20
|
attr_reader :answer
|
|
19
21
|
|
|
@@ -29,11 +31,17 @@ module Omakase
|
|
|
29
31
|
|
|
30
32
|
def name = "ruby"
|
|
31
33
|
|
|
34
|
+
# RubyLLM 2 tools cannot end the loop, so the strategy asks this between steps.
|
|
35
|
+
def done? = !@answer.nil? || @calls > @budget + 1
|
|
36
|
+
|
|
32
37
|
def execute(code:)
|
|
38
|
+
code = code.strip[FENCE, 1] || code
|
|
33
39
|
# Nothing bounds the provider's tool loop, so the budget does.
|
|
34
40
|
@calls += 1
|
|
35
41
|
return "No tool calls left — answer with what you have." if @calls == @budget + 1
|
|
36
|
-
return
|
|
42
|
+
return "Tool budget spent." if @calls > @budget + 1
|
|
43
|
+
# A later call in the same round must not act on an agent that has answered.
|
|
44
|
+
return "Answer already accepted." if @answer
|
|
37
45
|
|
|
38
46
|
outcome = @executor.call(@agent, code, timeout: @timeout)
|
|
39
47
|
Omakase.emit(:ruby, agent: @agent, code:, outcome:)
|
|
@@ -41,8 +49,8 @@ module Omakase
|
|
|
41
49
|
# The seam's contract, checked here so a wrong executor cannot reach the model.
|
|
42
50
|
raise Error, "executor must return a String or Executor::Answer, got #{outcome.class}" unless outcome.is_a?(Executor::Answer)
|
|
43
51
|
|
|
44
|
-
@answer = Executor::Answer.new(value: @schema.take(outcome.value))
|
|
45
|
-
|
|
52
|
+
@answer = Executor::Answer.new(value: @schema.take(outcome.value), printed: outcome.printed)
|
|
53
|
+
"Answer accepted."
|
|
46
54
|
rescue ContractError => e
|
|
47
55
|
# Off-contract answers are corrected inside the same loop, not by another request.
|
|
48
56
|
# Anything else — a broken executor, a bad configuration — is not the model's to fix.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Omakase
|
|
4
|
+
# The listener, printed for a human: `Omakase.listener = Omakase::Trace.new`.
|
|
5
|
+
# A run reads top to bottom — the call, the code the model wrote, the answer.
|
|
6
|
+
# Colour when the stream is a terminal, plain when it is a log.
|
|
7
|
+
class Trace
|
|
8
|
+
COLOURS = {generation: 36, ruby: 33, answer: 32, error: 31, mcp: 31}.freeze
|
|
9
|
+
LIMIT = 800
|
|
10
|
+
|
|
11
|
+
def initialize(io: $stderr)
|
|
12
|
+
@io = io
|
|
13
|
+
@colour = io.respond_to?(:tty?) && io.tty?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(event, agent:, **payload)
|
|
17
|
+
head, body = case event
|
|
18
|
+
when :generation then ["→ #{agent.class}##{payload[:name]}", inputs(payload[:inputs])]
|
|
19
|
+
when :ruby then ["· ruby", "#{payload[:code].strip}\n#{outcome(payload[:outcome])}"]
|
|
20
|
+
when :answer then ["← #{agent.class}##{payload[:name]}", truncate(payload[:value].inspect)]
|
|
21
|
+
when :error then ["✗ #{agent.class}##{payload[:name]}", truncate("#{payload[:error].class}: #{payload[:error].message}")]
|
|
22
|
+
# A down sidecar is not an error the run raises, so nothing else would say it.
|
|
23
|
+
when :mcp then ["! #{agent} mcp #{payload[:name]}", truncate(payload[:error].message)]
|
|
24
|
+
else return # a listener that raises takes the run down with it
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# A generation called from generated code sits inside its caller's.
|
|
28
|
+
indent = " " * [(Thread.current[Agent::RUNNING]&.size || 1) - 1, 0].max
|
|
29
|
+
@io.puts(indent + paint(event, head))
|
|
30
|
+
@io.puts(body.gsub(/^/, "#{indent} ")) unless body.empty?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def inputs(inputs) = inputs.map { |name, value| "#{name}: #{truncate(value.inspect)}" }.join("\n")
|
|
36
|
+
|
|
37
|
+
# An Answer is `finish(value)` ending the run — with anything printed before it,
|
|
38
|
+
# which the tool result never carries because the run is over. Otherwise the
|
|
39
|
+
# outcome is already the text the model reads.
|
|
40
|
+
def outcome(outcome)
|
|
41
|
+
return truncate(outcome.to_s) unless outcome.is_a?(Executor::Answer)
|
|
42
|
+
|
|
43
|
+
truncate([outcome.printed, "finish #{outcome.value.inspect}"].reject(&:empty?).join("\n"))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def truncate(text) = (text.length > LIMIT) ? "#{text[0, LIMIT]}…" : text
|
|
47
|
+
|
|
48
|
+
def paint(event, text) = @colour ? "\e[#{COLOURS.fetch(event)}m#{text}\e[0m" : text
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/omakase/type.rb
CHANGED
|
@@ -20,9 +20,9 @@ module Omakase
|
|
|
20
20
|
def code_only? = true
|
|
21
21
|
|
|
22
22
|
def take(value)
|
|
23
|
-
|
|
23
|
+
raise ContractError, "expected #{describe}, got #{value.class}" unless value.is_a?(@klass)
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
well_formed(value)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def definition
|
|
@@ -30,5 +30,18 @@ module Omakase
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
alias_method :json, :definition
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# An object that can say whether it is well-formed gets asked — ActiveModel,
|
|
37
|
+
# ActiveRecord, anything of that shape. The refusal reaches the model as an
|
|
38
|
+
# observation, so your own validations are what it has to satisfy, and they
|
|
39
|
+
# stay where you wrote them instead of being retyped into a prompt.
|
|
40
|
+
def well_formed(value)
|
|
41
|
+
return value unless value.respond_to?(:valid?) && value.respond_to?(:errors)
|
|
42
|
+
return value if value.valid?
|
|
43
|
+
|
|
44
|
+
raise ContractError, "#{@klass} is invalid: #{value.errors.full_messages.join("; ")}"
|
|
45
|
+
end
|
|
33
46
|
end
|
|
34
47
|
end
|
data/lib/omakase/version.rb
CHANGED
data/lib/omakase.rb
CHANGED
|
@@ -21,6 +21,8 @@ module Omakase
|
|
|
21
21
|
ContractError = Class.new(Error)
|
|
22
22
|
# The model or its provider failed. RubyLLM has already retried what it retries.
|
|
23
23
|
ProviderError = Class.new(Error)
|
|
24
|
+
# What RubyLLM.chat itself takes; every other option is a `with_*` call on the chat.
|
|
25
|
+
CHAT_ARGUMENTS = %i[model provider protocol assume_model_exists context].freeze
|
|
24
26
|
|
|
25
27
|
class << self
|
|
26
28
|
# Providers, keys, default model, timeouts, logging — all of it is RubyLLM's.
|
|
@@ -45,7 +47,8 @@ module Omakase
|
|
|
45
47
|
end
|
|
46
48
|
|
|
47
49
|
# Where generated code runs. Anything answering `call(agent, code, timeout:)`
|
|
48
|
-
# will do —
|
|
50
|
+
# will do — Executor::Subprocess is the reference: a child process, so a
|
|
51
|
+
# timeout cannot take this one with it.
|
|
49
52
|
def executor=(executor)
|
|
50
53
|
@executor = callable!(executor, "executor")
|
|
51
54
|
end
|
|
@@ -60,6 +63,35 @@ module Omakase
|
|
|
60
63
|
|
|
61
64
|
def embedder = @embedder ||= ->(text) { RubyLLM.embed(text).vectors }
|
|
62
65
|
|
|
66
|
+
# How an agent gets a chat when none was injected. Anything answering
|
|
67
|
+
# `call(**options)` will do — one line in test_helper.rb keeps a whole suite
|
|
68
|
+
# off the network, including the class-level calls a job makes.
|
|
69
|
+
def chat_factory=(factory)
|
|
70
|
+
@chat_factory = callable!(factory, "chat_factory")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def chat_factory = @chat_factory ||= method(:build_chat)
|
|
74
|
+
|
|
75
|
+
# `caching: true`, `thinking: {effort: :high}`, `temperature: 0.2` become the
|
|
76
|
+
# chat's own with_* calls. Caching is on unless you say `caching: false`: the
|
|
77
|
+
# tool loop resends the whole chat on every step.
|
|
78
|
+
def build_chat(**options)
|
|
79
|
+
chat = RubyLLM.chat(**options.slice(*CHAT_ARGUMENTS))
|
|
80
|
+
{caching: true, **options.except(*CHAT_ARGUMENTS)}.each do |name, value|
|
|
81
|
+
setter = :"with_#{name}"
|
|
82
|
+
raise Error, "unknown chat option #{name}: RubyLLM::Chat has no ##{setter}" unless chat.respond_to?(setter)
|
|
83
|
+
|
|
84
|
+
keywords = chat.method(setter).parameters.any? { |kind, _| kind == :keyrest }
|
|
85
|
+
case value
|
|
86
|
+
when true then chat.public_send(setter)
|
|
87
|
+
when Array then chat.public_send(setter, *value)
|
|
88
|
+
when Hash then keywords ? chat.public_send(setter, **value) : chat.public_send(setter, value)
|
|
89
|
+
else chat.public_send(setter, value)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
chat
|
|
93
|
+
end
|
|
94
|
+
|
|
63
95
|
# Every step, as it happens: a generation starts, model-written code runs,
|
|
64
96
|
# an answer lands. Anything answering `call(event, **payload)` will do —
|
|
65
97
|
# a logger, a tracer, a test. Nil, the default, costs nothing.
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: omakase-agents
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- eugeny
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ruby_llm
|
|
@@ -16,14 +16,14 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
19
|
+
version: '2.0'
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
26
|
+
version: '2.0'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: schematist
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -79,10 +79,12 @@ files:
|
|
|
79
79
|
- lib/omakase/request.rb
|
|
80
80
|
- lib/omakase/schema.rb
|
|
81
81
|
- lib/omakase/skills.rb
|
|
82
|
+
- lib/omakase/skills/how_to_act/SKILL.md
|
|
82
83
|
- lib/omakase/strategies.rb
|
|
83
84
|
- lib/omakase/strategies/code_act.rb
|
|
84
85
|
- lib/omakase/strategies/predict.rb
|
|
85
86
|
- lib/omakase/tools/ruby.rb
|
|
87
|
+
- lib/omakase/trace.rb
|
|
86
88
|
- lib/omakase/type.rb
|
|
87
89
|
- lib/omakase/version.rb
|
|
88
90
|
homepage: https://github.com/esshka/omakase
|