robot_lab-to 0.2.7 → 0.2.8
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/Rakefile +6 -108
- 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 +114 -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 +79 -32
- 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 +5 -0
- data/lib/robot_lab/to/tools/edit.rb +5 -0
- data/lib/robot_lab/to/tools/request_decision.rb +4 -0
- data/lib/robot_lab/to/tools/submit_result.rb +1 -0
- data/lib/robot_lab/to/version.rb +1 -1
- metadata +5 -2
|
@@ -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
|
|
|
@@ -14,6 +14,17 @@ module RobotLab
|
|
|
14
14
|
# not make alone, it is written to a decision file (see DecisionManager). A
|
|
15
15
|
# blocking decision either pauses the loop (decision_mode: wait) or stops the
|
|
16
16
|
# run for a later `--resume` (decision_mode: exit).
|
|
17
|
+
#
|
|
18
|
+
# :reek:TooManyMethods :reek:TooManyInstanceVariables -- this is the
|
|
19
|
+
# single object that owns the whole iteration lifecycle (setup, agent
|
|
20
|
+
# turn, eval gate, commit/rollback, decisions, tokens, signals). Splitting
|
|
21
|
+
# it apart would scatter state (@run, @git, @pending_commit_failure,
|
|
22
|
+
# @stop_requested, ...) across collaborators that would just pass it back
|
|
23
|
+
# and forth -- see the "Architecture" section of CLAUDE.md.
|
|
24
|
+
# :reek:RepeatedConditional -- @stop_requested, @pending_commit_failure,
|
|
25
|
+
# and decisions_enabled? are real orchestrator-wide state, legitimately
|
|
26
|
+
# checked at each point that must react to it; that is what a run loop's
|
|
27
|
+
# flags are for, not a sign the checks belong on another object.
|
|
17
28
|
class Orchestrator
|
|
18
29
|
SIGNAL_STOP = "graceful_stop"
|
|
19
30
|
|
|
@@ -43,10 +54,17 @@ module RobotLab
|
|
|
43
54
|
@decisions = nil
|
|
44
55
|
@decision_tool = nil
|
|
45
56
|
@injected_decisions = []
|
|
57
|
+
@backoff = nil
|
|
58
|
+
@builder = nil
|
|
59
|
+
@notes = nil
|
|
60
|
+
@stop_conditions = nil
|
|
46
61
|
@accounted_in = 0
|
|
47
62
|
@accounted_out = 0
|
|
48
63
|
end
|
|
49
64
|
|
|
65
|
+
# :reek:TooManyStatements -- top-level run/rescue/ensure sequence for the
|
|
66
|
+
# whole process; the rescue branches are already split into their own
|
|
67
|
+
# error-shaped clauses.
|
|
50
68
|
def run
|
|
51
69
|
@resume_run_id ? setup_resume : setup_run
|
|
52
70
|
install_signal_handlers
|
|
@@ -64,15 +82,18 @@ module RobotLab
|
|
|
64
82
|
@git&.reset_hard unless @pending_commit_failure
|
|
65
83
|
@logger.log("orchestrator:abort", reason: @abort_reason, permanent: true)
|
|
66
84
|
rescue => e
|
|
67
|
-
|
|
85
|
+
message = e.message
|
|
86
|
+
@abort_reason = "fatal: #{message}"
|
|
68
87
|
@git&.reset_hard unless @pending_commit_failure
|
|
69
|
-
@logger.log("orchestrator:fatal", error: e.class.to_s, message:
|
|
88
|
+
@logger.log("orchestrator:fatal", error: e.class.to_s, message: message)
|
|
70
89
|
ensure
|
|
71
90
|
finalize_run
|
|
72
91
|
end
|
|
73
92
|
|
|
74
93
|
private
|
|
75
94
|
|
|
95
|
+
# :reek:TooManyStatements -- the loop body is the iteration lifecycle in
|
|
96
|
+
# its literal execution order; splitting it further would hide that order.
|
|
76
97
|
def main_loop
|
|
77
98
|
loop do
|
|
78
99
|
break if @stop_requested
|
|
@@ -83,8 +104,9 @@ module RobotLab
|
|
|
83
104
|
@stop_conditions.before?
|
|
84
105
|
|
|
85
106
|
@run.iteration += 1
|
|
86
|
-
|
|
87
|
-
|
|
107
|
+
iteration = @run.iteration
|
|
108
|
+
@logger.log("iteration:start", iteration: iteration)
|
|
109
|
+
progress "iteration #{iteration} (#{@config.model})..."
|
|
88
110
|
|
|
89
111
|
result = execute_iteration
|
|
90
112
|
|
|
@@ -114,6 +136,8 @@ module RobotLab
|
|
|
114
136
|
end
|
|
115
137
|
end
|
|
116
138
|
|
|
139
|
+
# :reek:TooManyStatements -- one-time run bootstrap, each step required
|
|
140
|
+
# in this order (branch, run dir, Run record, logger, notes, collaborators).
|
|
117
141
|
def setup_run
|
|
118
142
|
@git = CommitManager.new
|
|
119
143
|
ensure_head!
|
|
@@ -143,6 +167,8 @@ module RobotLab
|
|
|
143
167
|
|
|
144
168
|
# Resume a prior run from its run.json — enables external scheduling (cron):
|
|
145
169
|
# each tick re-enters the loop on the same branch and advances or re-pauses.
|
|
170
|
+
# :reek:TooManyStatements -- mirrors #setup_run's bootstrap order for the
|
|
171
|
+
# resume path.
|
|
146
172
|
def setup_resume
|
|
147
173
|
@git = CommitManager.new
|
|
148
174
|
ensure_head!
|
|
@@ -152,7 +178,8 @@ module RobotLab
|
|
|
152
178
|
|
|
153
179
|
@run = Run.load(state_path)
|
|
154
180
|
@objective = @run.objective
|
|
155
|
-
@
|
|
181
|
+
branch = @run.branch
|
|
182
|
+
@git.checkout_branch(branch)
|
|
156
183
|
|
|
157
184
|
@logger.open(@run.log_path)
|
|
158
185
|
@notes = NotesManager.new(@run.notes_path) # append — do NOT re-setup (would clobber history)
|
|
@@ -160,8 +187,8 @@ module RobotLab
|
|
|
160
187
|
build_collaborators
|
|
161
188
|
|
|
162
189
|
@logger.log("orchestrator:resume", run_id: @run.run_id, iteration: @run.iteration,
|
|
163
|
-
branch:
|
|
164
|
-
progress "resumed run #{@run.run_id} at iteration #{@run.iteration} → branch #{
|
|
190
|
+
branch: branch, model: @config.model)
|
|
191
|
+
progress "resumed run #{@run.run_id} at iteration #{@run.iteration} → branch #{branch}"
|
|
165
192
|
end
|
|
166
193
|
|
|
167
194
|
# Shared setup used by both a fresh run and a resume.
|
|
@@ -202,11 +229,14 @@ module RobotLab
|
|
|
202
229
|
|
|
203
230
|
# Poll the decision files until every blocking decision is resolved, the run
|
|
204
231
|
# is signaled to stop, or decision_timeout elapses.
|
|
232
|
+
# :reek:TooManyStatements -- the three exit conditions (stopped, resolved,
|
|
233
|
+
# timed out) must all be checked inside one poll loop.
|
|
205
234
|
def wait_for_decisions(pending)
|
|
206
235
|
progress "awaiting human decision on #{pending.size} item(s):"
|
|
207
236
|
pending.each { |d| progress " #{d.path}" }
|
|
208
237
|
@logger.log("decision:wait:start", count: pending.size)
|
|
209
|
-
|
|
238
|
+
timeout = @config.decision_timeout
|
|
239
|
+
deadline = timeout ? monotonic + timeout.to_i : nil
|
|
210
240
|
|
|
211
241
|
loop do
|
|
212
242
|
return if @stop_requested
|
|
@@ -216,7 +246,7 @@ module RobotLab
|
|
|
216
246
|
break unless @decisions.blocking_pending?
|
|
217
247
|
|
|
218
248
|
if deadline && monotonic >= deadline
|
|
219
|
-
raise AbortError, "awaiting human decision (timed out after #{
|
|
249
|
+
raise AbortError, "awaiting human decision (timed out after #{timeout}s)"
|
|
220
250
|
end
|
|
221
251
|
end
|
|
222
252
|
|
|
@@ -238,6 +268,8 @@ module RobotLab
|
|
|
238
268
|
|
|
239
269
|
# The happy path: build a fresh robot, run it, and return its submitted
|
|
240
270
|
# result (or a not-submitted marker).
|
|
271
|
+
# :reek:TooManyStatements -- one call per lifecycle step (reset counters,
|
|
272
|
+
# build prompt/robot, run, nudge, persist decisions, return result).
|
|
241
273
|
def run_agent_iteration
|
|
242
274
|
@accounted_in = @accounted_out = 0
|
|
243
275
|
@submit_tool = Tools::SubmitResult.new
|
|
@@ -265,12 +297,13 @@ module RobotLab
|
|
|
265
297
|
def persist_raised_decisions
|
|
266
298
|
return unless @decision_tool
|
|
267
299
|
|
|
300
|
+
iteration = @run.iteration
|
|
268
301
|
@decision_tool.captured_requests.each do |req|
|
|
269
|
-
decision = @decisions.record(**req, iteration:
|
|
270
|
-
@notes.append_decision(decision,
|
|
302
|
+
decision = @decisions.record(**req, iteration: iteration)
|
|
303
|
+
@notes.append_decision(decision, iteration)
|
|
271
304
|
@logger.log("decision:raised", id: decision.id, blocking: decision.blocking,
|
|
272
305
|
question: decision.question)
|
|
273
|
-
progress "iteration #{
|
|
306
|
+
progress "iteration #{iteration} raised decision: #{decision.question}"
|
|
274
307
|
end
|
|
275
308
|
end
|
|
276
309
|
|
|
@@ -298,10 +331,11 @@ module RobotLab
|
|
|
298
331
|
# True when the iteration should be retried — within the retry budget and
|
|
299
332
|
# not stopping. Sleeps out the backoff as a side effect before returning.
|
|
300
333
|
def retry_after_backoff?
|
|
301
|
-
|
|
334
|
+
errors = @run.consecutive_errors
|
|
335
|
+
return false unless errors <= @config.max_retries && !@stop_requested
|
|
302
336
|
|
|
303
|
-
@logger.log("backoff:start", consecutive_errors:
|
|
304
|
-
@backoff.sleep_for(
|
|
337
|
+
@logger.log("backoff:start", consecutive_errors: errors)
|
|
338
|
+
@backoff.sleep_for(errors)
|
|
305
339
|
@logger.log("backoff:end")
|
|
306
340
|
true
|
|
307
341
|
end
|
|
@@ -367,8 +401,9 @@ module RobotLab
|
|
|
367
401
|
|
|
368
402
|
def handle_no_improvement(result, score)
|
|
369
403
|
iteration = @run.iteration
|
|
404
|
+
detail = score.detail
|
|
370
405
|
@git.reset_hard unless @pending_commit_failure
|
|
371
|
-
@notes.append_no_improvement(result,
|
|
406
|
+
@notes.append_no_improvement(result, detail, iteration)
|
|
372
407
|
# A non-improving iteration is valid work that just wasn't better -- NOT a
|
|
373
408
|
# failure. It counts toward the plateau (diminishing-returns) stop only, so
|
|
374
409
|
# it must NOT trip max_consecutive_failures, which is for the robot being
|
|
@@ -377,8 +412,8 @@ module RobotLab
|
|
|
377
412
|
# verdicts are common and normal.
|
|
378
413
|
@run.iterations_since_improvement += 1
|
|
379
414
|
@run.consecutive_errors = 0
|
|
380
|
-
@logger.log("iteration:no_improvement", iteration: iteration, detail:
|
|
381
|
-
progress "iteration #{iteration} no improvement (#{
|
|
415
|
+
@logger.log("iteration:no_improvement", iteration: iteration, detail: detail)
|
|
416
|
+
progress "iteration #{iteration} no improvement (#{detail}) — rolled back"
|
|
382
417
|
end
|
|
383
418
|
|
|
384
419
|
# A resolved decision that was injected into this successful iteration has
|
|
@@ -405,14 +440,16 @@ module RobotLab
|
|
|
405
440
|
score
|
|
406
441
|
end
|
|
407
442
|
|
|
443
|
+
# :reek:TooManyStatements -- rollback + record + report, one statement each.
|
|
408
444
|
def handle_gate_failure(result, score)
|
|
445
|
+
iteration = @run.iteration
|
|
409
446
|
@git.reset_hard unless @pending_commit_failure
|
|
410
|
-
@notes.append_verify_failure(result, score.output.to_s,
|
|
447
|
+
@notes.append_verify_failure(result, score.output.to_s, iteration)
|
|
411
448
|
@run.consecutive_failures += 1
|
|
412
449
|
@run.consecutive_errors = 0
|
|
413
450
|
@run.iterations_since_improvement += 1
|
|
414
|
-
@logger.log("iteration:gate_failure", iteration:
|
|
415
|
-
progress "iteration #{
|
|
451
|
+
@logger.log("iteration:gate_failure", iteration: iteration)
|
|
452
|
+
progress "iteration #{iteration} verification failed — rolled back"
|
|
416
453
|
nil
|
|
417
454
|
end
|
|
418
455
|
|
|
@@ -421,19 +458,22 @@ module RobotLab
|
|
|
421
458
|
# the code, re-verifying after each attempt. Only fall back to a full
|
|
422
459
|
# rollback once the repair budget is spent — a near-miss no longer discards
|
|
423
460
|
# all the work, and nothing commits until the gate is green.
|
|
461
|
+
# :reek:TooManyStatements -- repair/re-verify loop with its own rescue;
|
|
462
|
+
# each statement is a distinct step of one repair attempt.
|
|
424
463
|
def repair_until_gate(result, score)
|
|
425
|
-
budget
|
|
464
|
+
budget = @config.max_verify_repairs.to_i
|
|
465
|
+
iteration = @run.iteration
|
|
426
466
|
return [result, score] if budget.zero? || @robot.nil?
|
|
427
467
|
|
|
428
468
|
budget.times do |i|
|
|
429
469
|
break if @stop_requested
|
|
430
470
|
|
|
431
|
-
@logger.log("eval:repair", iteration:
|
|
432
|
-
progress "iteration #{
|
|
471
|
+
@logger.log("eval:repair", iteration: iteration, attempt: i + 1)
|
|
472
|
+
progress "iteration #{iteration}: verification failed — repairing (#{i + 1}/#{budget})"
|
|
433
473
|
begin
|
|
434
474
|
run_robot_with_interrupt(@robot, repair_prompt(score))
|
|
435
475
|
rescue StandardError => e
|
|
436
|
-
@logger.log("eval:repair_error", iteration:
|
|
476
|
+
@logger.log("eval:repair_error", iteration: iteration, message: e.message)
|
|
437
477
|
break
|
|
438
478
|
end
|
|
439
479
|
result = @submit_tool.captured_result || result
|
|
@@ -459,16 +499,20 @@ module RobotLab
|
|
|
459
499
|
end
|
|
460
500
|
|
|
461
501
|
def handle_failure(result)
|
|
502
|
+
iteration = @run.iteration
|
|
462
503
|
@git.reset_hard unless @pending_commit_failure
|
|
463
|
-
@notes.append_failure(result,
|
|
504
|
+
@notes.append_failure(result, iteration)
|
|
464
505
|
@run.consecutive_failures += 1
|
|
465
506
|
@run.consecutive_errors = 0
|
|
466
507
|
@run.iterations_since_improvement += 1
|
|
467
|
-
@logger.log("iteration:failure", iteration:
|
|
468
|
-
progress "iteration #{
|
|
508
|
+
@logger.log("iteration:failure", iteration: iteration, summary: result.summary)
|
|
509
|
+
progress "iteration #{iteration} failed: #{result.summary}"
|
|
469
510
|
end
|
|
470
511
|
|
|
512
|
+
# :reek:TooManyStatements -- commit path plus its own CommitFailedError
|
|
513
|
+
# rescue; the two branches (success, queue-for-repair) are each tight.
|
|
471
514
|
def attempt_commit(result)
|
|
515
|
+
iteration = @run.iteration
|
|
472
516
|
@git.add_all
|
|
473
517
|
return unless @git.staged?
|
|
474
518
|
|
|
@@ -476,16 +520,17 @@ module RobotLab
|
|
|
476
520
|
@git.commit(message)
|
|
477
521
|
@run.commits += 1
|
|
478
522
|
@pending_commit_failure = nil
|
|
479
|
-
@logger.log("commit:success", iteration:
|
|
480
|
-
progress "iteration #{
|
|
523
|
+
@logger.log("commit:success", iteration: iteration, message: message)
|
|
524
|
+
progress "iteration #{iteration} committed: #{message}"
|
|
481
525
|
rescue CommitFailedError => e
|
|
482
526
|
@pending_commit_failure = e
|
|
483
527
|
@run.consecutive_failures += 1
|
|
484
528
|
@run.consecutive_errors = 0
|
|
485
|
-
@logger.log("commit:failed", iteration:
|
|
486
|
-
progress "iteration #{
|
|
529
|
+
@logger.log("commit:failed", iteration: iteration, output: e.output)
|
|
530
|
+
progress "iteration #{iteration} commit failed — queued for repair"
|
|
487
531
|
end
|
|
488
532
|
|
|
533
|
+
# :reek:FeatureEnvy -- formatting the commit message from result's own fields.
|
|
489
534
|
def commit_message(result)
|
|
490
535
|
if @config.commit_format == "conventional"
|
|
491
536
|
type = result.respond_to?(:type) ? (result.type || "chore") : "chore"
|
|
@@ -565,6 +610,8 @@ module RobotLab
|
|
|
565
610
|
end
|
|
566
611
|
end
|
|
567
612
|
|
|
613
|
+
# :reek:TooManyStatements -- runs the robot on its own Thread so a signal
|
|
614
|
+
# can interrupt it; each statement manages that thread's lifecycle.
|
|
568
615
|
def run_robot_with_interrupt(robot, task = @objective)
|
|
569
616
|
thread_error = nil
|
|
570
617
|
result = nil
|
|
@@ -8,6 +8,8 @@ module RobotLab
|
|
|
8
8
|
@config = config
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
# :reek:TooManyStatements -- each line is one optional prompt section, in
|
|
12
|
+
# the exact order they appear; a data-table would hide that ordering.
|
|
11
13
|
def build(run, notes_content, workspace: nil, pending_commit_failure: nil, resolved_decisions: [])
|
|
12
14
|
sections = [role_section(run), notes_section(run, notes_content)]
|
|
13
15
|
sections << workspace_section(workspace) if workspace && !workspace.empty?
|
data/lib/robot_lab/to/run.rb
CHANGED
|
@@ -13,6 +13,8 @@ module RobotLab
|
|
|
13
13
|
:commits, :input_tokens, :output_tokens,
|
|
14
14
|
:last_score_value, :iterations_since_improvement
|
|
15
15
|
|
|
16
|
+
# :reek:ControlParameter -- run_dir/decisions_path fall back to a sibling
|
|
17
|
+
# path when omitted; that's a constructor default, not a behavior branch.
|
|
16
18
|
def initialize(run_id:, objective:, branch:, base_commit:, notes_path:, log_path:,
|
|
17
19
|
run_dir: nil, decisions_path: nil)
|
|
18
20
|
@run_id = run_id
|
|
@@ -38,6 +40,7 @@ module RobotLab
|
|
|
38
40
|
def elapsed_seconds = Time.now - started_at
|
|
39
41
|
def state_path = @run_dir.join("run.json")
|
|
40
42
|
|
|
43
|
+
# :reek:FeatureEnvy -- decomposing our own elapsed_seconds into h/m/s.
|
|
41
44
|
def elapsed_human
|
|
42
45
|
secs = elapsed_seconds.to_i
|
|
43
46
|
h = secs / 3600
|
|
@@ -71,14 +74,11 @@ module RobotLab
|
|
|
71
74
|
base_commit: data[:base_commit], notes_path: data[:notes_path],
|
|
72
75
|
log_path: data[:log_path], run_dir: data[:run_dir],
|
|
73
76
|
decisions_path: data[:decisions_path])
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
run.
|
|
79
|
-
run.output_tokens = data[:output_tokens].to_i
|
|
80
|
-
run.last_score_value = data[:last_score_value]
|
|
81
|
-
run.iterations_since_improvement = data[:iterations_since_improvement].to_i
|
|
77
|
+
%i[iteration consecutive_failures consecutive_errors commits input_tokens
|
|
78
|
+
output_tokens iterations_since_improvement].each do |key|
|
|
79
|
+
run.public_send(:"#{key}=", data[key].to_i)
|
|
80
|
+
end
|
|
81
|
+
run.last_score_value = data[:last_score_value]
|
|
82
82
|
run.instance_variable_set(:@started_at, Time.parse(data[:started_at])) if data[:started_at]
|
|
83
83
|
run
|
|
84
84
|
end
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module RobotLab
|
|
4
4
|
module To
|
|
5
5
|
# Evaluates all stop conditions against the current run state.
|
|
6
|
+
# :reek:RepeatedConditional -- each `limit` is an unrelated config value
|
|
7
|
+
# (max_iterations/max_tokens/stop_on_plateau) nil-guarded in its own check.
|
|
6
8
|
class StopConditions
|
|
7
9
|
def initialize(config, run)
|
|
8
10
|
@config = config
|
|
@@ -38,17 +40,19 @@ module RobotLab
|
|
|
38
40
|
private
|
|
39
41
|
|
|
40
42
|
def check_max_iterations
|
|
41
|
-
|
|
42
|
-
return unless
|
|
43
|
+
limit = @config.max_iterations
|
|
44
|
+
return unless limit
|
|
45
|
+
return unless @run.iteration >= limit
|
|
43
46
|
|
|
44
|
-
raise AbortError, "max iterations reached (#{
|
|
47
|
+
raise AbortError, "max iterations reached (#{limit})"
|
|
45
48
|
end
|
|
46
49
|
|
|
47
50
|
def check_max_tokens
|
|
48
|
-
|
|
49
|
-
return unless
|
|
51
|
+
limit = @config.max_tokens
|
|
52
|
+
return unless limit
|
|
53
|
+
return unless @run.total_tokens >= limit
|
|
50
54
|
|
|
51
|
-
raise AbortError, "max tokens reached (#{
|
|
55
|
+
raise AbortError, "max tokens reached (#{limit})"
|
|
52
56
|
end
|
|
53
57
|
|
|
54
58
|
def check_consecutive_failures
|
|
@@ -25,6 +25,8 @@ module RobotLab
|
|
|
25
25
|
@default_timeout = timeout
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
# :reek:ControlParameter -- falling back to the instance default when
|
|
29
|
+
# the LLM omits the optional `timeout` argument.
|
|
28
30
|
def execute(command:, timeout: nil, **)
|
|
29
31
|
out, status = run(command, (timeout || @default_timeout).to_i)
|
|
30
32
|
format_result(out, status)
|
|
@@ -34,6 +36,9 @@ module RobotLab
|
|
|
34
36
|
# status_string is "0".."n" for an exit code, or "timeout"/"error: ...".
|
|
35
37
|
#
|
|
36
38
|
# @return [Array(String, String)]
|
|
39
|
+
# :reek:TooManyStatements :reek:NestedIterators -- spawn, read
|
|
40
|
+
# concurrently, wait-or-kill, join: each step is one statement and the
|
|
41
|
+
# streaming reader inherently nests a loop inside its own Thread.
|
|
37
42
|
def run(command, timeout)
|
|
38
43
|
out_str = +""
|
|
39
44
|
status = nil
|
|
@@ -19,6 +19,11 @@ module RobotLab
|
|
|
19
19
|
param :new_text, type: "string", desc: "Replacement text"
|
|
20
20
|
param :replace_all, type: "boolean", desc: "Replace every occurrence", required: false
|
|
21
21
|
|
|
22
|
+
# :reek:BooleanParameter -- replace_all is a tool param the LLM sets;
|
|
23
|
+
# the `param` declarations above fix this signature.
|
|
24
|
+
# :reek:FeatureEnvy -- inspecting #apply's own return value.
|
|
25
|
+
# :reek:TooManyStatements -- validate, transform, write, report; each
|
|
26
|
+
# step is one statement in a tool #execute contract method.
|
|
22
27
|
def execute(path:, old_text:, new_text:, replace_all: false, **)
|
|
23
28
|
resolved = File.expand_path(path, Dir.pwd)
|
|
24
29
|
return "Error: file not found: #{path}" unless File.file?(resolved)
|
|
@@ -44,6 +44,10 @@ module RobotLab
|
|
|
44
44
|
@captured_requests ||= []
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
# :reek:BooleanParameter :reek:ControlParameter -- blocking is both
|
|
48
|
+
# stored data and legitimately changes the guidance message returned
|
|
49
|
+
# to the robot; the `param` declarations above fix this signature.
|
|
50
|
+
# :reek:LongParameterList -- one field per tool param declared above.
|
|
47
51
|
def execute(question:, situation: "", options: [], recommendation: "", blocking: false, **)
|
|
48
52
|
(@captured_requests ||= []) << {
|
|
49
53
|
question: question,
|
|
@@ -34,6 +34,7 @@ module RobotLab
|
|
|
34
34
|
|
|
35
35
|
attr_reader :captured_result
|
|
36
36
|
|
|
37
|
+
# :reek:LongParameterList -- one field per tool param declared above.
|
|
37
38
|
def execute(success:, summary:, key_changes: [], key_learnings: [], should_fully_stop: nil, **)
|
|
38
39
|
@captured_result = IterationResult.new(
|
|
39
40
|
success: success,
|
data/lib/robot_lab/to/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: robot_lab-to
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dewayne VanHoozer
|
|
@@ -57,6 +57,7 @@ files:
|
|
|
57
57
|
- ".loki"
|
|
58
58
|
- ".quality/reek_baseline.txt"
|
|
59
59
|
- ".rubocop.yml"
|
|
60
|
+
- Archspec.rb
|
|
60
61
|
- CHANGELOG.md
|
|
61
62
|
- CLAUDE.md
|
|
62
63
|
- COMMITS.md
|
|
@@ -85,6 +86,7 @@ files:
|
|
|
85
86
|
- docs/local-models/ollama.md
|
|
86
87
|
- docs/local-models/tools.md
|
|
87
88
|
- docs/reference/architecture.md
|
|
89
|
+
- examples/.envrc
|
|
88
90
|
- examples/01_basic_usage/.gitignore
|
|
89
91
|
- examples/01_basic_usage/README.md
|
|
90
92
|
- examples/01_basic_usage/basic_usage.rb
|
|
@@ -104,6 +106,7 @@ files:
|
|
|
104
106
|
- examples/04_prose/prompts_dir/section_criteria.md
|
|
105
107
|
- examples/04_prose/prompts_dir/sections_objective.md
|
|
106
108
|
- examples/04_prose/prose_run.rb
|
|
109
|
+
- examples/common.rb
|
|
107
110
|
- lib/robot_lab/to.rb
|
|
108
111
|
- lib/robot_lab/to/atomic_file.rb
|
|
109
112
|
- lib/robot_lab/to/backoff.rb
|
|
@@ -169,7 +172,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
169
172
|
- !ruby/object:Gem::Version
|
|
170
173
|
version: '0'
|
|
171
174
|
requirements: []
|
|
172
|
-
rubygems_version: 4.0.
|
|
175
|
+
rubygems_version: 4.0.20
|
|
173
176
|
specification_version: 4
|
|
174
177
|
summary: Autonomous overnight agent loop for RobotLab — run robots while you sleep.
|
|
175
178
|
test_files: []
|