lemans 1.3.1 → 1.3.2
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 +5 -0
- data/exe/lemans-remote +100 -7
- data/lib/lemans/agents/miniswen_installed.rb +6 -4
- data/lib/lemans/trial.rb +11 -1
- data/lib/lemans/version.rb +1 -1
- data/lib/miniswen/ruby_llm.rb +2 -2
- data/lib/miniswen/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: db9e22f2b2098371943cde85a366d70ff205b267719b67974484c5240752cd59
|
|
4
|
+
data.tar.gz: 707a10355b3fb212740b2839b51a1b346e8e6165a4aed5403f9c763045ffa6b8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f1472766d86a65544e6b525124fcd0afb828fa36c6a3c5ee1d4bb2d9f24972edb2d844abfdfc03735c6a9cd81b6bbda00b791f782b42cb7d523becdd01f764f1
|
|
7
|
+
data.tar.gz: 95d42dbaf57d92ace03b1bffdcf05316fd7244a5abb3fb74a5611ea15a4c8de4bdd86d6e9ab21b2b4bcf4606bce5ab11e5bf44fb372ae74c810d4c38b1c1b465
|
data/CHANGELOG.md
CHANGED
data/exe/lemans-remote
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
# Then watch, fetch, and clean up:
|
|
32
32
|
#
|
|
33
33
|
# exe/lemans-remote status [--history] [--running | --complete] [-W [INTERVAL]]
|
|
34
|
+
# exe/lemans-remote logs RUN_ID [--tail N]
|
|
34
35
|
# exe/lemans-remote pull-runs [RUN_ID ...] [--all] [--dry-run]
|
|
35
36
|
# exe/lemans-remote drop-orphans [--min-age 10m]
|
|
36
37
|
# exe/lemans-remote clobber RUN_ID ... | --all
|
|
@@ -213,11 +214,7 @@ module LemansRemote # :nodoc: all
|
|
|
213
214
|
|
|
214
215
|
def pack(to:)
|
|
215
216
|
Dir.mktmpdir("lemans-remote-stage") do |stage|
|
|
216
|
-
|
|
217
|
-
next if EXCLUDES.include?(child.basename.to_s)
|
|
218
|
-
|
|
219
|
-
FileUtils.cp_r(child, File.join(stage, child.basename.to_s))
|
|
220
|
-
end
|
|
217
|
+
stage_files(stage)
|
|
221
218
|
prune_tasks(stage) if @selected_tasks
|
|
222
219
|
_, err, status = Open3.capture3("tar", "-czf", to, "-C", stage, ".")
|
|
223
220
|
raise "could not pack the bench: #{err}" unless status.success?
|
|
@@ -227,6 +224,44 @@ module LemansRemote # :nodoc: all
|
|
|
227
224
|
|
|
228
225
|
private
|
|
229
226
|
|
|
227
|
+
# Every sandbox gets a copy, so a git bench ships what git would: tracked
|
|
228
|
+
# files plus untracked ones .gitignore lets through.
|
|
229
|
+
def stage_files(stage)
|
|
230
|
+
files = git_files
|
|
231
|
+
return copy_children(stage) unless files
|
|
232
|
+
|
|
233
|
+
files.each do |relative|
|
|
234
|
+
next if EXCLUDES.include?(relative.split("/", 2).first)
|
|
235
|
+
|
|
236
|
+
source = @bench.root.join(relative)
|
|
237
|
+
# The index still lists a file deleted from disk
|
|
238
|
+
next unless File.exist?(source) || File.symlink?(source)
|
|
239
|
+
|
|
240
|
+
destination = File.join(stage, relative)
|
|
241
|
+
FileUtils.mkdir_p(File.dirname(destination))
|
|
242
|
+
FileUtils.copy_entry(source, destination)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def copy_children(stage)
|
|
247
|
+
@bench.root.children.each do |child|
|
|
248
|
+
next if EXCLUDES.include?(child.basename.to_s)
|
|
249
|
+
|
|
250
|
+
FileUtils.cp_r(child, File.join(stage, child.basename.to_s))
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# nil when the bench is not a git checkout; a nested repository lists as a
|
|
255
|
+
# bare directory, and is left out
|
|
256
|
+
def git_files
|
|
257
|
+
out, _err, status = Open3.capture3(
|
|
258
|
+
"git", "-C", @bench.root.to_s, "ls-files", "-z", "--cached", "--others", "--exclude-standard"
|
|
259
|
+
)
|
|
260
|
+
return nil unless status.success?
|
|
261
|
+
|
|
262
|
+
out.split("\0").reject { it.end_with?("/") }
|
|
263
|
+
end
|
|
264
|
+
|
|
230
265
|
def prune_tasks(stage)
|
|
231
266
|
tasks_rel = @bench.tasks_dir.relative_path_from(@bench.root).to_s
|
|
232
267
|
staged_tasks = File.join(stage, tasks_rel)
|
|
@@ -415,6 +450,14 @@ module LemansRemote # :nodoc: all
|
|
|
415
450
|
body.empty? ? [] : JSON.parse(body)
|
|
416
451
|
end
|
|
417
452
|
|
|
453
|
+
def run_log(sandbox, run_id)
|
|
454
|
+
remote = "/vault/#{run_id}/run.log"
|
|
455
|
+
response = sandbox.process.exec(command: "cat #{Shellwords.escape(remote)}", timeout: 120)
|
|
456
|
+
raise "no log in the vault for #{run_id} (never launched, or still running on a sandbox that is gone)" unless response.exit_code.zero?
|
|
457
|
+
|
|
458
|
+
response.result.to_s
|
|
459
|
+
end
|
|
460
|
+
|
|
418
461
|
def archive_size(sandbox, run_id)
|
|
419
462
|
remote = "/vault/#{run_id}/runs.tar.gz"
|
|
420
463
|
response = sandbox.process.exec(command: "du -h #{Shellwords.escape(remote)}", timeout: 60)
|
|
@@ -467,6 +510,7 @@ module LemansRemote # :nodoc: all
|
|
|
467
510
|
SETUP_TIMEOUT_SEC = 300
|
|
468
511
|
TRANSFER_TIMEOUT_SEC = 900
|
|
469
512
|
TIMED_OUT = 124
|
|
513
|
+
CREATE_ATTEMPTS = 3
|
|
470
514
|
|
|
471
515
|
def initialize(bench:, snapshot:, run_id:, tasks:, models:, extra_args:, extra_env:,
|
|
472
516
|
timeout_sec:, runs_dir:, keep:, sync:, shell:)
|
|
@@ -518,8 +562,38 @@ module LemansRemote # :nodoc: all
|
|
|
518
562
|
0
|
|
519
563
|
end
|
|
520
564
|
|
|
565
|
+
# Daytona sometimes never starts a sandbox it accepted; the SDK gives up
|
|
566
|
+
# after a minute and the half-made sandbox stays behind under this run's
|
|
567
|
+
# labels, listed as running until Daytona flags it, then stale forever.
|
|
521
568
|
def create_sandbox(volume = nil)
|
|
522
|
-
|
|
569
|
+
attempt = 0
|
|
570
|
+
begin
|
|
571
|
+
attempt += 1
|
|
572
|
+
LemansRemote.client.create(sandbox_params(volume))
|
|
573
|
+
rescue *Backend::Retries::SDK_ERRORS => e
|
|
574
|
+
drop_half_created
|
|
575
|
+
raise if attempt >= CREATE_ATTEMPTS
|
|
576
|
+
|
|
577
|
+
say :retry, "sandbox did not start (#{e.message}), attempt #{attempt + 1}/#{CREATE_ATTEMPTS}", :yellow
|
|
578
|
+
retry
|
|
579
|
+
end
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
# Runs from the rescue of a failed creation, so it must not raise: an
|
|
583
|
+
# exception here would replace the creation error and end the retries.
|
|
584
|
+
def drop_half_created
|
|
585
|
+
query = ::Daytona::ListSandboxesQuery.new(labels: { RUN_ID_LABEL => @run_id })
|
|
586
|
+
LemansRemote.client.list(query).each do |sandbox|
|
|
587
|
+
sandbox.delete
|
|
588
|
+
rescue StandardError => e
|
|
589
|
+
warn "lemans-remote: could not delete half-created sandbox #{sandbox.id}: #{e.message}"
|
|
590
|
+
end
|
|
591
|
+
rescue StandardError => e
|
|
592
|
+
warn "lemans-remote: could not list the sandboxes of #{@run_id}: #{e.message}"
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def sandbox_params(volume)
|
|
596
|
+
::Daytona::CreateSandboxFromSnapshotParams.new(
|
|
523
597
|
snapshot: @snapshot,
|
|
524
598
|
env_vars: env_vars,
|
|
525
599
|
labels: {
|
|
@@ -533,7 +607,6 @@ module LemansRemote # :nodoc: all
|
|
|
533
607
|
ttl_minutes: (@timeout_sec / 60.0).ceil + 60,
|
|
534
608
|
volumes: volume ? [ Vault.mount_param(volume) ] : nil
|
|
535
609
|
)
|
|
536
|
-
LemansRemote.client.create(params)
|
|
537
610
|
end
|
|
538
611
|
|
|
539
612
|
def env_vars
|
|
@@ -897,6 +970,26 @@ module LemansRemote # :nodoc: all
|
|
|
897
970
|
raise Thor::Error, "lemans-remote: #{e.message}"
|
|
898
971
|
end
|
|
899
972
|
|
|
973
|
+
desc "logs RUN_ID", "Print a run's lemans output (live from its sandbox, or from the vault once it finished)"
|
|
974
|
+
option :tail, type: :numeric, desc: "Only the last N lines"
|
|
975
|
+
def logs(run_id)
|
|
976
|
+
row = Fleet.rows.find { it.run_id == run_id }
|
|
977
|
+
text =
|
|
978
|
+
if row&.state == DaytonaApiClient::SandboxState::STARTED
|
|
979
|
+
row.sandbox.process.exec(command: "cat #{REMOTE_LOG} 2>/dev/null", timeout: 60).result.to_s
|
|
980
|
+
elsif row && Fleet::LIVE_STATES.include?(row.state)
|
|
981
|
+
# The vault gets the log when the run exits, and a sandbox still
|
|
982
|
+
# coming up has nothing to read yet
|
|
983
|
+
raise "#{run_id} is #{row.state} — it has not started logging yet"
|
|
984
|
+
else
|
|
985
|
+
with_vault { |vault, sandbox| vault.run_log(sandbox, run_id) }
|
|
986
|
+
end
|
|
987
|
+
text = text.lines.last(options[:tail].to_i).join if options[:tail]
|
|
988
|
+
say text
|
|
989
|
+
rescue RuntimeError => e
|
|
990
|
+
raise Thor::Error, "lemans-remote: #{e.message}"
|
|
991
|
+
end
|
|
992
|
+
|
|
900
993
|
map "pull-runs" => :pull_runs
|
|
901
994
|
desc "pull-runs [RUN_IDS...]", "Download archived runs from the vault into the local runs directory"
|
|
902
995
|
option :all, type: :boolean, default: false, desc: "Pull every completed run, even ones already pulled"
|
|
@@ -14,8 +14,9 @@ module Lemans
|
|
|
14
14
|
NAME = "miniswen-installed"
|
|
15
15
|
RESULTS_PATH = "/tmp/lemans-miniswen.result.json"
|
|
16
16
|
INSTALL_TIMEOUT_SEC = 300
|
|
17
|
-
# The CLI enforces max-time itself
|
|
18
|
-
#
|
|
17
|
+
# The CLI enforces max-time itself, but between steps only: a command
|
|
18
|
+
# started just before the deadline runs to its own exec timeout first,
|
|
19
|
+
# and the outer exec must outlast that too. The slack covers startup.
|
|
19
20
|
EXEC_SLACK_SEC = 60
|
|
20
21
|
|
|
21
22
|
def install(_task, environment)
|
|
@@ -31,8 +32,7 @@ module Lemans
|
|
|
31
32
|
# An in-sandbox run self-reports: everything but the verifier's reward
|
|
32
33
|
# comes from a file the sandbox wrote.
|
|
33
34
|
def obtain_result(task, environment)
|
|
34
|
-
run = environment.exec(command_for(task), timeout:
|
|
35
|
-
env: provider_env(environment))
|
|
35
|
+
run = environment.exec(command_for(task), timeout: outer_timeout, env: provider_env(environment))
|
|
36
36
|
|
|
37
37
|
begin
|
|
38
38
|
Tempfile.create(%w[miniswen .result.json]) do |file|
|
|
@@ -49,6 +49,8 @@ module Lemans
|
|
|
49
49
|
|
|
50
50
|
attr_reader :raw_result
|
|
51
51
|
|
|
52
|
+
def outer_timeout = profile.timeout + profile.exec_timeout + EXEC_SLACK_SEC
|
|
53
|
+
|
|
52
54
|
# A missing credential fails the run before the sandbox executes
|
|
53
55
|
# anything: it is the operator's configuration to fix, not a trial result.
|
|
54
56
|
def provider_env(environment)
|
data/lib/lemans/trial.rb
CHANGED
|
@@ -77,6 +77,11 @@ module Lemans
|
|
|
77
77
|
rescue InfrastructureError, ::Miniswen::InfrastructureError => e
|
|
78
78
|
# Mark the failure here, where the agent phase is still known
|
|
79
79
|
result.failed!(:agent_error, e.message)
|
|
80
|
+
collect_patch!
|
|
81
|
+
raise
|
|
82
|
+
rescue ::Miniswen::AccountingError
|
|
83
|
+
# Classified by the outer rescue; the work is still on disk
|
|
84
|
+
collect_patch!
|
|
80
85
|
raise
|
|
81
86
|
end
|
|
82
87
|
|
|
@@ -86,6 +91,7 @@ module Lemans
|
|
|
86
91
|
|
|
87
92
|
if response.error?
|
|
88
93
|
result.failed!(:agent_error, response.error)
|
|
94
|
+
collect_patch!
|
|
89
95
|
return result
|
|
90
96
|
end
|
|
91
97
|
|
|
@@ -97,7 +103,7 @@ module Lemans
|
|
|
97
103
|
|
|
98
104
|
check_cost_limit!
|
|
99
105
|
|
|
100
|
-
|
|
106
|
+
collect_patch!
|
|
101
107
|
if step_task.final_step?
|
|
102
108
|
patch.compile!(result, store) if task.multistep? && store
|
|
103
109
|
# Don't index the final verification
|
|
@@ -146,6 +152,10 @@ module Lemans
|
|
|
146
152
|
|
|
147
153
|
private
|
|
148
154
|
|
|
155
|
+
def collect_patch!
|
|
156
|
+
patch.collect!(result, store, path: with_step_index("agent.patch")) if store
|
|
157
|
+
end
|
|
158
|
+
|
|
149
159
|
def save_trajectory!(trajectory)
|
|
150
160
|
return unless trajectory && store
|
|
151
161
|
|
data/lib/lemans/version.rb
CHANGED
data/lib/miniswen/ruby_llm.rb
CHANGED
|
@@ -8,9 +8,9 @@ RubyLLM.configure do |config|
|
|
|
8
8
|
config.logger = Logger.new(IO::NULL) unless ENV["MINISWEN_DEBUG"] == "1"
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
#
|
|
11
|
+
# About a minute of retries for egress blips (1, 2, 4, 8, 16, 32s plus jitter)
|
|
12
12
|
RubyLLM.configure do |config|
|
|
13
|
-
config.max_retries =
|
|
13
|
+
config.max_retries = 6
|
|
14
14
|
config.retry_interval = 1
|
|
15
15
|
end
|
|
16
16
|
|
data/lib/miniswen/version.rb
CHANGED