Module: Omakase::Executor

Defined in:
lib/omakase/executor.rb

Overview

Runs model-written Ruby in the agent's own context. ponytail: instance_eval is not a sandbox — see Omakase.executor to swap it.

Defined Under Namespace

Classes: Answer

Constant Summary collapse

SOURCE =
"(generated)"
RESULT =
:omakase_result
OUTPUT =
:omakase_output
TIMEOUT =
30
MAX_OUTPUT =
4_000

Class Method Summary collapse

Class Method Details

.call(agent, code, timeout: TIMEOUT) ⇒ Object



18
19
20
21
22
23
24
25
26
27
# File 'lib/omakase/executor.rb', line 18

def call(agent, code, timeout: TIMEOUT)
  printed = StringIO.new
  answer = catch(RESULT) do
    value = capturing(printed) { Timeout.timeout(timeout) { agent.instance_eval(code, SOURCE, 1) } }
    return observation([printed.string.chomp, "=> #{value.inspect}"])
  end
  Answer.new(value: answer)
rescue ScriptError, StandardError => e
  observation([printed.string.chomp, failure(e, code)])
end

.capturing(io) ⇒ Object

Thread-local, so concurrent agents never share a buffer. Agent#puts reads it.



43
44
45
46
47
48
49
# File 'lib/omakase/executor.rb', line 43

def capturing(io)
  previous = Thread.current[OUTPUT]
  Thread.current[OUTPUT] = io
  yield
ensure
  Thread.current[OUTPUT] = previous
end

.failure(error, code) ⇒ Object

The model can only fix what it can locate, so point at the line.



30
31
32
33
34
# File 'lib/omakase/executor.rb', line 30

def failure(error, code)
  line = error.backtrace&.grep(/\A#{Regexp.escape(SOURCE)}:\d+/)&.first&.slice(/:(\d+)/, 1)&.to_i
  source = code.lines[line - 1]&.strip if line&.positive?
  ["#{error.class}: #{error.message}", ("line #{line}: #{source}" if source)].compact.join("\n")
end

.observation(parts) ⇒ Object



36
37
38
39
40
# File 'lib/omakase/executor.rb', line 36

def observation(parts)
  text = parts.reject(&:empty?).join("\n")
  text = "#{text[0, MAX_OUTPUT]}\n… (truncated)" if text.length > MAX_OUTPUT
  text.empty? ? "(no output)" : text
end