robot_lab-to 0.2.7 → 0.3.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/.envrc +6 -0
- data/Archspec.rb +34 -0
- data/CHANGELOG.md +4 -0
- data/CLAUDE.md +3 -2
- data/README.md +9 -7
- data/Rakefile +6 -108
- data/docs/concepts/stop-conditions.md +3 -3
- data/docs/configuration/cli.md +4 -5
- data/docs/configuration/index.md +2 -3
- data/docs/configuration/settings.md +7 -7
- data/docs/getting-started/installation.md +5 -5
- data/docs/index.md +3 -3
- data/docs/local-models/index.md +14 -17
- data/docs/local-models/lm-studio.md +92 -0
- data/docs/reference/architecture.md +4 -4
- data/examples/.envrc +8 -0
- data/examples/01_basic_usage/README.md +14 -15
- data/examples/01_basic_usage/basic_usage.rb +20 -57
- data/examples/02_advanced_usage/README.md +10 -7
- data/examples/02_advanced_usage/advanced_usage.rb +23 -31
- data/examples/03_scored/scored_run.rb +19 -51
- data/examples/04_prose/README.md +15 -14
- data/examples/04_prose/prose_run.rb +22 -38
- data/examples/common.rb +111 -0
- data/lib/robot_lab/to/cli.rb +61 -34
- data/lib/robot_lab/to/commit_manager.rb +4 -0
- data/lib/robot_lab/to/config.rb +12 -0
- data/lib/robot_lab/to/decision_manager.rb +12 -1
- data/lib/robot_lab/to/exit_summary.rb +17 -16
- data/lib/robot_lab/to/guards/checkpoint.rb +6 -3
- data/lib/robot_lab/to/guards/quality_monitor.rb +4 -2
- data/lib/robot_lab/to/guards/run_store.rb +4 -2
- data/lib/robot_lab/to/notes_manager.rb +3 -0
- data/lib/robot_lab/to/orchestrator.rb +85 -34
- data/lib/robot_lab/to/prompt_builder.rb +2 -0
- data/lib/robot_lab/to/run.rb +8 -8
- data/lib/robot_lab/to/stop_conditions.rb +10 -6
- data/lib/robot_lab/to/tools/bash.rb +7 -2
- data/lib/robot_lab/to/tools/edit.rb +9 -4
- data/lib/robot_lab/to/tools/read.rb +3 -3
- data/lib/robot_lab/to/tools/request_decision.rb +14 -10
- data/lib/robot_lab/to/tools/submit_result.rb +12 -11
- data/lib/robot_lab/to/tools/write.rb +2 -2
- data/lib/robot_lab/to/version.rb +1 -1
- data/lib/robot_lab/to.rb +22 -0
- data/mkdocs.yml +1 -1
- metadata +10 -7
- data/docs/local-models/ollama.md +0 -122
data/examples/common.rb
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# ===========================================================================
|
|
4
|
+
# common.rb — shared RubyLLM setup/teardown for the robot_lab-to example demos
|
|
5
|
+
# ===========================================================================
|
|
6
|
+
#
|
|
7
|
+
# Every example under examples/ requires this file (examples/.envrc supplies
|
|
8
|
+
# the RLTO_* / LMS_BASE_URL defaults) and calls `setup` before talking to
|
|
9
|
+
# RubyLLM -- once for a single-model example, or once per model role for an
|
|
10
|
+
# example with more than one (e.g. 04_prose's doer + judge).
|
|
11
|
+
#
|
|
12
|
+
# actual_provider = setup(provider: PROVIDER, model: MODEL)
|
|
13
|
+
# RobotLab::To.run(objective, provider: actual_provider, model: MODEL, ...)
|
|
14
|
+
#
|
|
15
|
+
# "lms" is the ruby_llm-providers-lms gem's native LM Studio provider.
|
|
16
|
+
# When `provider` is "lms", setup:
|
|
17
|
+
# 1. points the :lms provider at LMS_BASE_URL
|
|
18
|
+
# 2. starts the LM Studio server if it isn't already running
|
|
19
|
+
# 3. loads `model` into LM Studio if it isn't already loaded
|
|
20
|
+
# and returns :lms -- the RubyLLM-recognized provider to pass to RobotLab.
|
|
21
|
+
# Any other provider (a cloud one) passes straight through untouched; setup
|
|
22
|
+
# does nothing else for it (the example configures its own API key as before).
|
|
23
|
+
#
|
|
24
|
+
# teardown stops the LM Studio server, but ONLY if setup started it here -- a
|
|
25
|
+
# server you already had running (the LM Studio app, another example) is left
|
|
26
|
+
# alone. It runs automatically via an at_exit hook; examples never call it.
|
|
27
|
+
# ===========================================================================
|
|
28
|
+
|
|
29
|
+
require "json"
|
|
30
|
+
require "logger"
|
|
31
|
+
require "open3"
|
|
32
|
+
require "uri"
|
|
33
|
+
|
|
34
|
+
require "ruby_llm"
|
|
35
|
+
require "ruby_llm/providers/lms"
|
|
36
|
+
|
|
37
|
+
LMS_BASE_URL = ENV.fetch("LMS_BASE_URL", "http://localhost:1234/v1")
|
|
38
|
+
|
|
39
|
+
@lms_server_started_by_us = false
|
|
40
|
+
|
|
41
|
+
def setup(provider: ENV.fetch("RLTO_PROVIDER", "lms"), model: ENV.fetch("RLTO_MODEL", "qwen/qwen3.8-27b"))
|
|
42
|
+
provider = provider.to_sym
|
|
43
|
+
return provider unless provider == :lms
|
|
44
|
+
|
|
45
|
+
configure_lms!
|
|
46
|
+
ensure_lms_server_running!
|
|
47
|
+
ensure_lms_model_loaded!(model)
|
|
48
|
+
:lms
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def teardown
|
|
52
|
+
return unless @lms_server_started_by_us
|
|
53
|
+
|
|
54
|
+
puts "Stopping the LM Studio server (started for this run)…"
|
|
55
|
+
Open3.capture2e("lms", "server", "stop")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
at_exit { teardown }
|
|
59
|
+
|
|
60
|
+
# --- internals ---------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
# Point the :lms provider at the LM Studio endpoint. Local models are assumed
|
|
63
|
+
# to exist, so no registry refresh is needed.
|
|
64
|
+
def configure_lms!
|
|
65
|
+
RubyLLM.configure do |c|
|
|
66
|
+
c.lms_api_base = LMS_BASE_URL
|
|
67
|
+
c.request_timeout = 600
|
|
68
|
+
end
|
|
69
|
+
RubyLLM.logger.level = Logger::ERROR
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def lms_server_running?
|
|
73
|
+
out, status = Open3.capture2("lms", "server", "status", "--json")
|
|
74
|
+
status.success? && JSON.parse(out)["running"] == true
|
|
75
|
+
rescue StandardError
|
|
76
|
+
false
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def ensure_lms_server_running!
|
|
80
|
+
return if lms_server_running?
|
|
81
|
+
|
|
82
|
+
puts "Starting the LM Studio server…"
|
|
83
|
+
port = URI.parse(LMS_BASE_URL).port.to_s
|
|
84
|
+
out, status = Open3.capture2e("lms", "server", "start", "--port", port)
|
|
85
|
+
abort "Could not start the LM Studio server:\n#{out}" unless status.success? && lms_server_running?
|
|
86
|
+
|
|
87
|
+
@lms_server_started_by_us = true
|
|
88
|
+
rescue Errno::ENOENT
|
|
89
|
+
abort "The `lms` CLI was not found. Install LM Studio (https://lmstudio.ai) and " \
|
|
90
|
+
"run `lms bootstrap` to put it on your PATH."
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def lms_model_loaded?(model)
|
|
94
|
+
out, status = Open3.capture2("lms", "ps", "--json")
|
|
95
|
+
return false unless status.success?
|
|
96
|
+
|
|
97
|
+
JSON.parse(out).any? { |m| m["modelKey"] == model || m["identifier"] == model }
|
|
98
|
+
rescue StandardError
|
|
99
|
+
false
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def ensure_lms_model_loaded!(model)
|
|
103
|
+
return if lms_model_loaded?(model)
|
|
104
|
+
|
|
105
|
+
puts "Loading #{model} into LM Studio (first run only)…"
|
|
106
|
+
out, status = Open3.capture2e("lms", "load", model, "-y")
|
|
107
|
+
abort "Could not load #{model} in LM Studio:\n#{out}" unless status.success?
|
|
108
|
+
rescue Errno::ENOENT
|
|
109
|
+
abort "The `lms` CLI was not found. Install LM Studio (https://lmstudio.ai) and " \
|
|
110
|
+
"run `lms bootstrap` to put it on your PATH."
|
|
111
|
+
end
|
data/lib/robot_lab/to/cli.rb
CHANGED
|
@@ -46,6 +46,17 @@ module RobotLab
|
|
|
46
46
|
"Seconds between polls while waiting on a decision (default: 30)", :decision_wait_poll]
|
|
47
47
|
].freeze
|
|
48
48
|
|
|
49
|
+
# Boolean flags with a fixed value when passed (no argument). Flags
|
|
50
|
+
# needing bespoke behavior (protect-path, require-improvement, help)
|
|
51
|
+
# stay in #add_flag_options.
|
|
52
|
+
BOOLEAN_FLAGS = [
|
|
53
|
+
["--no-decisions", "Disable the request_decision tool for this run", :decisions_enabled, false],
|
|
54
|
+
["--local-guards", "Add built-in file tools + small-model guardrails (for local models)", :local_guards, true],
|
|
55
|
+
["--no-stream", "Disable response streaming (required for local Ollama tool calls)", :stream, false],
|
|
56
|
+
["--debug", "Enable verbose JSONL logging to stderr", :debug, true],
|
|
57
|
+
["--version", "Print version and exit", :version, true]
|
|
58
|
+
].freeze
|
|
59
|
+
|
|
49
60
|
def self.run(argv = ARGV)
|
|
50
61
|
new.run(argv)
|
|
51
62
|
end
|
|
@@ -53,32 +64,33 @@ module RobotLab
|
|
|
53
64
|
def run(argv)
|
|
54
65
|
return run_decisions(argv[1..] || []) if argv.first == "decisions"
|
|
55
66
|
|
|
56
|
-
opts
|
|
57
|
-
parser = build_parser(opts)
|
|
58
|
-
args = parser.parse!(argv.dup)
|
|
67
|
+
opts, parser, args = parse_options(argv)
|
|
59
68
|
|
|
60
|
-
if opts[:version]
|
|
61
|
-
|
|
62
|
-
return
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
if opts[:resume]
|
|
66
|
-
return RobotLab::To.resume(opts[:resume], **opts.except(:version, :resume))
|
|
67
|
-
end
|
|
69
|
+
return puts("robot-to #{VERSION}") if opts[:version]
|
|
70
|
+
return RobotLab::To.resume(opts[:resume], **opts.except(:version, :resume)) if opts[:resume]
|
|
68
71
|
|
|
69
72
|
objective = args.first || read_stdin_objective
|
|
70
|
-
if objective.nil? || objective.strip.empty?
|
|
71
|
-
# $stderr.puts, not warn: warn is silenced when $VERBOSE is nil.
|
|
72
|
-
$stderr.puts "Error: objective required (pass as argument or via stdin)"
|
|
73
|
-
$stderr.puts parser
|
|
74
|
-
exit 1
|
|
75
|
-
end
|
|
73
|
+
abort_missing_objective!(parser) if objective.nil? || objective.strip.empty?
|
|
76
74
|
|
|
77
75
|
RobotLab::To.run(objective.strip, **opts.except(:version, :resume))
|
|
78
76
|
end
|
|
79
77
|
|
|
80
78
|
private
|
|
81
79
|
|
|
80
|
+
def parse_options(argv)
|
|
81
|
+
opts = {}
|
|
82
|
+
parser = build_parser(opts)
|
|
83
|
+
args = parser.parse!(argv.dup)
|
|
84
|
+
[opts, parser, args]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def abort_missing_objective!(parser)
|
|
88
|
+
# $stderr.puts, not warn: warn is silenced when $VERBOSE is nil.
|
|
89
|
+
$stderr.puts "Error: objective required (pass as argument or via stdin)"
|
|
90
|
+
$stderr.puts parser
|
|
91
|
+
exit 1
|
|
92
|
+
end
|
|
93
|
+
|
|
82
94
|
# `robot-to decisions [run_id]` — list pending decisions and their file
|
|
83
95
|
# paths so a human knows what needs resolving before resuming.
|
|
84
96
|
def run_decisions(args)
|
|
@@ -96,17 +108,19 @@ module RobotLab
|
|
|
96
108
|
def print_decisions(run_id, manager)
|
|
97
109
|
pending = manager.pending
|
|
98
110
|
resolved = manager.resolved_open
|
|
99
|
-
|
|
100
|
-
puts ""
|
|
111
|
+
|
|
101
112
|
if pending.empty? && resolved.empty?
|
|
102
|
-
puts "
|
|
113
|
+
puts "Run #{run_id}\n\nNo open decisions."
|
|
103
114
|
return
|
|
104
115
|
end
|
|
116
|
+
|
|
117
|
+
puts "Run #{run_id}\n\n"
|
|
105
118
|
list_group("Pending (awaiting your answer)", pending)
|
|
106
119
|
list_group("Resolved (not yet consumed)", resolved)
|
|
107
|
-
puts
|
|
108
|
-
|
|
109
|
-
|
|
120
|
+
puts <<~MSG
|
|
121
|
+
Resolve a pending decision by editing its file: set `status: resolved`
|
|
122
|
+
and fill `resolution:`, then run `robot-to --resume #{run_id}`.
|
|
123
|
+
MSG
|
|
110
124
|
end
|
|
111
125
|
|
|
112
126
|
def list_group(title, decisions)
|
|
@@ -126,6 +140,7 @@ module RobotLab
|
|
|
126
140
|
.sort.map { |p| File.basename(p) }.last
|
|
127
141
|
end
|
|
128
142
|
|
|
143
|
+
# :reek:FeatureEnvy -- configuring the OptionParser instance being built.
|
|
129
144
|
def build_parser(opts)
|
|
130
145
|
OptionParser.new do |p|
|
|
131
146
|
p.banner = "Usage: robot-to [objective] [options]\n " \
|
|
@@ -136,30 +151,42 @@ module RobotLab
|
|
|
136
151
|
p.separator ""
|
|
137
152
|
p.separator "Options:"
|
|
138
153
|
|
|
139
|
-
|
|
140
|
-
p.on(flag, type, desc) { |v| opts[key] = v }
|
|
141
|
-
end
|
|
142
|
-
|
|
154
|
+
add_value_options(p, opts)
|
|
143
155
|
add_flag_options(p, opts)
|
|
144
156
|
end
|
|
145
157
|
end
|
|
146
158
|
|
|
159
|
+
# :reek:NestedIterators -- one .on registration per option, each with its
|
|
160
|
+
# own value-assignment callback; that's the OptionParser API shape.
|
|
161
|
+
def add_value_options(parser, opts)
|
|
162
|
+
VALUE_OPTIONS.each do |flag, type, desc, key|
|
|
163
|
+
parser.on(flag, type, desc) { |v| opts[key] = v }
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# :reek:FeatureEnvy -- registering flags on the OptionParser being built.
|
|
147
168
|
# Boolean and terminal flags (each has bespoke behavior).
|
|
148
169
|
def add_flag_options(parser, opts)
|
|
170
|
+
add_require_improvement_option(parser, opts)
|
|
171
|
+
add_protect_path_option(parser, opts)
|
|
172
|
+
BOOLEAN_FLAGS.each { |flag, desc, key, value| parser.on(flag, desc) { opts[key] = value } }
|
|
173
|
+
add_help_option(parser)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def add_require_improvement_option(parser, opts)
|
|
149
177
|
parser.on("--[no-]require-improvement",
|
|
150
178
|
"Roll back gate-passing iterations that don't improve (default: on)") do |v|
|
|
151
179
|
opts[:require_improvement] = v
|
|
152
180
|
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def add_protect_path_option(parser, opts)
|
|
153
184
|
parser.on("--protect-path GLOB", "Lock a grader file from robot edits (repeatable)") do |v|
|
|
154
185
|
(opts[:protect_paths] ||= []) << v
|
|
155
186
|
end
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
end
|
|
160
|
-
parser.on("--no-stream", "Disable response streaming (required for local Ollama tool calls)") { opts[:stream] = false }
|
|
161
|
-
parser.on("--debug", "Enable verbose JSONL logging to stderr") { opts[:debug] = true }
|
|
162
|
-
parser.on("--version", "Print version and exit") { opts[:version] = true }
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def add_help_option(parser)
|
|
163
190
|
parser.on("-h", "--help", "Show this help") do
|
|
164
191
|
puts parser
|
|
165
192
|
exit
|
|
@@ -8,6 +8,9 @@ module RobotLab
|
|
|
8
8
|
#
|
|
9
9
|
# All subprocess calls use explicit argv arrays (no shell interpolation).
|
|
10
10
|
# GIT_TERMINAL_PROMPT=0 prevents credential prompts from hanging the loop.
|
|
11
|
+
# :reek:RepeatedConditional -- each `status.success?` belongs to a distinct
|
|
12
|
+
# Open3.capture3 call with its own failure handling; there is no shared
|
|
13
|
+
# condition to extract.
|
|
11
14
|
class CommitManager
|
|
12
15
|
GIT_ENV = { "GIT_TERMINAL_PROMPT" => "0" }.freeze
|
|
13
16
|
|
|
@@ -72,6 +75,7 @@ module RobotLab
|
|
|
72
75
|
parse_diff_stat(out)
|
|
73
76
|
end
|
|
74
77
|
|
|
78
|
+
# :reek:FeatureEnvy -- inherent to building/reading/appending a Pathname.
|
|
75
79
|
def add_to_local_exclude(entry)
|
|
76
80
|
exclude = Pathname.new(@work_dir).join(".git", "info", "exclude")
|
|
77
81
|
exclude.parent.mkpath
|
data/lib/robot_lab/to/config.rb
CHANGED
|
@@ -11,6 +11,15 @@ module RobotLab
|
|
|
11
11
|
# 2. User config file (~/.config/robot_lab/to.yml)
|
|
12
12
|
# 3. Environment variables (ROBOT_LAB_TO_*)
|
|
13
13
|
# 4. Constructor keyword arguments (CLI overrides)
|
|
14
|
+
# :reek:TooManyInstanceVariables -- a flat CLI-override bag for ~24
|
|
15
|
+
# independent settings; each ivar is genuinely one distinct setting.
|
|
16
|
+
# :reek:InstanceVariableAssumption -- the defaults.yml-backed ivars
|
|
17
|
+
# (provider, model, max_consecutive_failures, max_submit_nudges,
|
|
18
|
+
# max_verify_repairs, verify_timeout, run_dir, commit_format, local_guards,
|
|
19
|
+
# stream, debug, decisions_enabled, decision_mode, decision_wait_poll,
|
|
20
|
+
# decision_timeout) ARE assigned -- by `super()` (MywayConfig::Base /
|
|
21
|
+
# Anyway::Config), before any of this class's own code runs. Do NOT
|
|
22
|
+
# pre-nil them here: that overwrites the real loaded values with nil.
|
|
14
23
|
class Config < MywayConfig::Base
|
|
15
24
|
config_name :robot_lab_to
|
|
16
25
|
env_prefix :robot_lab_to
|
|
@@ -34,6 +43,9 @@ module RobotLab
|
|
|
34
43
|
|
|
35
44
|
def initialize(**overrides)
|
|
36
45
|
super()
|
|
46
|
+
# CLI-only, no YAML default (nil means "no limit / not set") -- unlike
|
|
47
|
+
# the defaults.yml-backed ivars above, super() never touches these.
|
|
48
|
+
@max_iterations = @max_tokens = @stop_when = @verify_command = nil
|
|
37
49
|
@eval = @eval_measure = @eval_target = nil
|
|
38
50
|
@require_improvement = @stop_on_plateau = nil
|
|
39
51
|
@eval_judge_model = @eval_spec = @eval_floor = nil
|
|
@@ -40,6 +40,11 @@ module RobotLab
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
# Persist a decision the robot raised. Returns the parsed Decision.
|
|
43
|
+
# :reek:BooleanParameter -- blocking is stored data (front matter), not a
|
|
44
|
+
# behavior switch.
|
|
45
|
+
# :reek:ControlParameter -- the ternary only coerces blocking to true/false.
|
|
46
|
+
# :reek:LongParameterList { max_params: 6 } -- one field per front-matter
|
|
47
|
+
# attribute; a hash would just move the same six names elsewhere.
|
|
43
48
|
def record(question:, situation: "", options: [], recommendation: "", blocking: false, iteration: 0)
|
|
44
49
|
id = generate_id
|
|
45
50
|
path = @dir.join("#{id}.md")
|
|
@@ -57,6 +62,7 @@ module RobotLab
|
|
|
57
62
|
def pending = all.select(&:pending?)
|
|
58
63
|
# resolved but not yet closed
|
|
59
64
|
def resolved_open = all.select(&:resolved?)
|
|
65
|
+
# :reek:FeatureEnvy -- filtering by a Decision's own predicates.
|
|
60
66
|
def blocking_pending = all.select { |d| d.pending? && d.blocking? }
|
|
61
67
|
def blocking_pending? = blocking_pending.any?
|
|
62
68
|
|
|
@@ -66,6 +72,7 @@ module RobotLab
|
|
|
66
72
|
# Mark a resolved decision closed: its resolution has been delivered to a
|
|
67
73
|
# robot and committed, so it should no longer be re-injected. Flips only
|
|
68
74
|
# the status line, preserving whatever the human wrote in the body.
|
|
75
|
+
# :reek:FeatureEnvy -- reading/rewriting the decision's own file.
|
|
69
76
|
def close(decision)
|
|
70
77
|
raw = File.read(decision.path)
|
|
71
78
|
AtomicFile.write(decision.path, flip_status(raw, "closed"))
|
|
@@ -82,8 +89,10 @@ module RobotLab
|
|
|
82
89
|
raw.sub(/^status:.*$/, "status: #{status}")
|
|
83
90
|
end
|
|
84
91
|
|
|
92
|
+
# :reek:LongParameterList { max_params: 7 } -- one field per front-matter
|
|
93
|
+
# attribute, mirroring #record.
|
|
85
94
|
def render_new(id:, question:, situation:, options:, recommendation:, blocking:, iteration:)
|
|
86
|
-
options_md = options.empty? ? "(none provided)\n" : options.each_with_index.map { |
|
|
95
|
+
options_md = options.empty? ? "(none provided)\n" : options.each_with_index.map { |opt, i| "#{i + 1}. #{opt}" }.join("\n") + "\n"
|
|
87
96
|
# Build the front matter with YAML.dump so question/recommendation are
|
|
88
97
|
# escaped correctly (they can contain colons, quotes, etc.).
|
|
89
98
|
front = {
|
|
@@ -116,6 +125,7 @@ module RobotLab
|
|
|
116
125
|
|
|
117
126
|
# Split a file into (front_matter_hash, body). Returns a Decision or nil
|
|
118
127
|
# when the file is unreadable.
|
|
128
|
+
# :reek:FeatureEnvy -- building a Decision from its own front matter hash.
|
|
119
129
|
def parse(path)
|
|
120
130
|
text = File.read(path)
|
|
121
131
|
fm, body = split_front_matter(text)
|
|
@@ -138,6 +148,7 @@ module RobotLab
|
|
|
138
148
|
nil
|
|
139
149
|
end
|
|
140
150
|
|
|
151
|
+
# :reek:FeatureEnvy -- slicing the raw file text into front matter/body.
|
|
141
152
|
def split_front_matter(text)
|
|
142
153
|
if text.start_with?("---\n") && (close_idx = text.index("\n---", 4))
|
|
143
154
|
raw_fm = text[4...close_idx]
|
|
@@ -11,14 +11,9 @@ module RobotLab
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def print
|
|
14
|
-
stat = @run.base_commit ? diff_stat : { insertions: 0, deletions: 0, files: 0 }
|
|
15
|
-
aborted = !@abort_reason.nil?
|
|
16
|
-
good_iters = @run.commits
|
|
17
|
-
fail_iters = @run.iteration - good_iters
|
|
18
|
-
|
|
19
14
|
puts ""
|
|
20
|
-
|
|
21
|
-
print_counters
|
|
15
|
+
@abort_reason ? print_aborted_header : print_completed_header
|
|
16
|
+
print_counters
|
|
22
17
|
print_paths
|
|
23
18
|
print_next_steps
|
|
24
19
|
puts ""
|
|
@@ -26,17 +21,21 @@ module RobotLab
|
|
|
26
21
|
|
|
27
22
|
private
|
|
28
23
|
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
24
|
+
def print_aborted_header
|
|
25
|
+
puts "robot-to stopped — #{@config.model} — #{@run.elapsed_human}"
|
|
26
|
+
puts "Reason: #{@abort_reason}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def print_completed_header
|
|
30
|
+
puts "robot-to complete — #{@config.model} — #{@run.elapsed_human}"
|
|
31
|
+
puts "Branch: #{@run.branch}"
|
|
37
32
|
end
|
|
38
33
|
|
|
39
|
-
def print_counters
|
|
34
|
+
def print_counters
|
|
35
|
+
good_iters = @run.commits
|
|
36
|
+
fail_iters = @run.iteration - good_iters
|
|
37
|
+
stat = diff_stat
|
|
38
|
+
|
|
40
39
|
puts ""
|
|
41
40
|
puts "Iterations: #{@run.iteration} total (#{good_iters} good / #{fail_iters} failed)"
|
|
42
41
|
puts "Tokens: #{comma(@run.total_tokens)} (#{comma(@run.input_tokens)} in / #{comma(@run.output_tokens)} out)"
|
|
@@ -61,6 +60,8 @@ module RobotLab
|
|
|
61
60
|
end
|
|
62
61
|
|
|
63
62
|
def diff_stat
|
|
63
|
+
return { insertions: 0, deletions: 0, files: 0 } unless @run.base_commit
|
|
64
|
+
|
|
64
65
|
git = CommitManager.new
|
|
65
66
|
git.diff_stat(@run.base_commit)
|
|
66
67
|
rescue StandardError
|
|
@@ -48,7 +48,12 @@ module RobotLab
|
|
|
48
48
|
|
|
49
49
|
tracked << path
|
|
50
50
|
FileUtils.mkdir_p(dir)
|
|
51
|
-
|
|
51
|
+
write_backup(path, File.join(dir, safe_name(path)))
|
|
52
|
+
rescue SystemCallError, IOError
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def write_backup(path, dest)
|
|
52
57
|
if File.exist?(path)
|
|
53
58
|
FileUtils.cp(path, dest)
|
|
54
59
|
dest
|
|
@@ -56,8 +61,6 @@ module RobotLab
|
|
|
56
61
|
File.write("#{dest}.absent", "")
|
|
57
62
|
"#{dest}.absent"
|
|
58
63
|
end
|
|
59
|
-
rescue SystemCallError, IOError
|
|
60
|
-
nil
|
|
61
64
|
end
|
|
62
65
|
|
|
63
66
|
# @return [String] directory for this run's checkpoints (created)
|
|
@@ -73,8 +73,9 @@ module RobotLab
|
|
|
73
73
|
return :empty_response if text.to_s.strip.empty? && calls.empty?
|
|
74
74
|
|
|
75
75
|
calls.each do |c|
|
|
76
|
-
|
|
77
|
-
return
|
|
76
|
+
name = c[:name]
|
|
77
|
+
return :empty_tool_name if name.to_s.empty?
|
|
78
|
+
return "unknown_tool:#{name}" if known.any? && !known.include?(name)
|
|
78
79
|
end
|
|
79
80
|
|
|
80
81
|
return :repeated_tool_call if repeated?(calls, previous)
|
|
@@ -83,6 +84,7 @@ module RobotLab
|
|
|
83
84
|
end
|
|
84
85
|
|
|
85
86
|
# True when any current call exactly matches any previous call.
|
|
87
|
+
# :reek:NestedIterators -- an all-pairs comparison needs both loops.
|
|
86
88
|
def repeated?(calls, previous)
|
|
87
89
|
return false if calls.empty? || previous.empty?
|
|
88
90
|
|
|
@@ -22,8 +22,10 @@ module RobotLab
|
|
|
22
22
|
|
|
23
23
|
# Fetch the value for `key`, initializing to `default` when unset.
|
|
24
24
|
def fetch(key, default = nil)
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
value = Thread.current[key]
|
|
26
|
+
return value unless value.nil?
|
|
27
|
+
|
|
28
|
+
Thread.current[key] = default
|
|
27
29
|
end
|
|
28
30
|
end
|
|
29
31
|
end
|
|
@@ -13,6 +13,7 @@ module RobotLab
|
|
|
13
13
|
@path = Pathname.new(path)
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
# :reek:FeatureEnvy -- interpolating the run's own fields into the header.
|
|
16
17
|
def setup(run)
|
|
17
18
|
AtomicFile.write(@path, <<~HEADER)
|
|
18
19
|
# robot-to run: #{run.run_id}
|
|
@@ -84,6 +85,7 @@ module RobotLab
|
|
|
84
85
|
MD
|
|
85
86
|
end
|
|
86
87
|
|
|
88
|
+
# :reek:FeatureEnvy -- interpolating the error's own class/message.
|
|
87
89
|
def append_error(error, iteration)
|
|
88
90
|
append(<<~MD)
|
|
89
91
|
|
|
@@ -93,6 +95,7 @@ module RobotLab
|
|
|
93
95
|
MD
|
|
94
96
|
end
|
|
95
97
|
|
|
98
|
+
# :reek:FeatureEnvy -- interpolating the decision's own fields.
|
|
96
99
|
def append_decision(decision, iteration)
|
|
97
100
|
append(<<~MD)
|
|
98
101
|
|