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 — Subprocess isolates a crash, not File.
Defined Under Namespace
Modules: Subprocess Classes: Answer
Constant Summary collapse
- SOURCE =
"(generated)"- RESULT =
:omakase_result- OUTPUT =
:omakase_output- TIMEOUT =
30- TRACE =
/\A#{Regexp.escape(SOURCE)}:\d+/- MAX_OUTPUT =
4_000
Class Method Summary collapse
- .call(agent, code, timeout: TIMEOUT) ⇒ Object
-
.capturing(io) ⇒ Object
Thread-local, so concurrent agents never share a buffer.
-
.evaluate(agent, code) ⇒ Object
Inside a generation the inputs are locals; outside one, plain instance_eval.
-
.failure(error, code) ⇒ Object
The model can only fix what it can locate, so point at the line.
-
.observation(parts) ⇒ Object
Head and tail: the error or the value comes last, and it is what the model needs next.
Class Method Details
.call(agent, code, timeout: TIMEOUT) ⇒ Object
23 24 25 26 27 28 29 30 31 32 33 34 35 |
# File 'lib/omakase/executor.rb', line 23 def call(agent, code, timeout: TIMEOUT) printed = StringIO.new answer = catch(RESULT) do value = capturing(printed) { Timeout.timeout(timeout) { evaluate(agent, code) } } return observation([printed.string.chomp, "=> #{value.inspect}"]) end Answer.new(value: answer, printed: printed.string.chomp) rescue ScriptError, StandardError => e observation([printed.string.chomp, failure(e, code)]) rescue SystemExit # In process, exit would end the host. exit! cannot be caught: Subprocess covers that. observation([printed.string.chomp, "exit is not allowed — answer with finish(value)"]) end |
.capturing(io) ⇒ Object
Thread-local, so concurrent agents never share a buffer. Agent#puts reads it.
63 64 65 66 67 68 69 |
# File 'lib/omakase/executor.rb', line 63 def capturing(io) previous = Thread.current[OUTPUT] Thread.current[OUTPUT] = io yield ensure Thread.current[OUTPUT] = previous end |
.evaluate(agent, code) ⇒ Object
Inside a generation the inputs are locals; outside one, plain instance_eval.
38 39 40 41 |
# File 'lib/omakase/executor.rb', line 38 def evaluate(agent, code) scope = agent.respond_to?(:omakase_scope, true) && agent.send(:omakase_scope) scope ? scope.eval(code, SOURCE, 1) : agent.instance_eval(code, SOURCE, 1) end |
.failure(error, code) ⇒ Object
The model can only fix what it can locate, so point at the line. A SyntaxError has no generated frame to point from.
45 46 47 48 49 50 51 |
# File 'lib/omakase/executor.rb', line 45 def failure(error, code) = "#{error.class}: #{error.}" line = error.backtrace.grep(TRACE).first&.slice(/:(\d+)/, 1)&.to_i return unless line "#{}\nline #{line}: #{code.lines[line - 1].to_s.strip}" end |
.observation(parts) ⇒ Object
Head and tail: the error or the value comes last, and it is what the model needs next.
54 55 56 57 58 59 60 |
# File 'lib/omakase/executor.rb', line 54 def observation(parts) text = parts.reject(&:empty?).join("\n") return text if text.length <= MAX_OUTPUT half = MAX_OUTPUT / 2 "#{text[0, half]}\n… (#{text.length - MAX_OUTPUT} characters truncated)\n#{text[-half..]}" end |