lemans 0.2.3 → 1.0.0.pre.1
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/README.md +30 -10
- data/exe/lemans-remote +94 -29
- data/lib/lemans/agent.rb +39 -0
- data/lib/lemans/agents/miniswen.rb +28 -25
- data/lib/lemans/agents/miniswen_installed.rb +13 -9
- data/lib/lemans/agents/nop.rb +3 -3
- data/lib/lemans/agents/oracle.rb +8 -8
- data/lib/lemans/cli/board_reporter.rb +16 -16
- data/lib/lemans/cli/progress_reporter.rb +19 -19
- data/lib/lemans/cli/report/aggregate.rb +117 -0
- data/lib/lemans/cli/report.rb +164 -0
- data/lib/lemans/cli.rb +58 -63
- data/lib/lemans/clobber.rb +17 -55
- data/lib/lemans/config/agent.rb +51 -0
- data/lib/lemans/config/conversion.rb +62 -0
- data/lib/lemans/config/environment.rb +39 -0
- data/lib/lemans/config/image_spec.rb +42 -0
- data/lib/lemans/config/network_policy.rb +55 -0
- data/lib/lemans/config/revision.rb +42 -0
- data/lib/lemans/config/setup.rb +57 -0
- data/lib/lemans/config/tree_digest.rb +26 -0
- data/lib/lemans/config/verifier.rb +72 -0
- data/lib/lemans/config.rb +107 -0
- data/lib/lemans/environment.rb +54 -0
- data/lib/lemans/environments/daytona/faraday_transfer.rb +172 -0
- data/lib/lemans/environments/daytona/sdk_tweaks.rb +1 -1
- data/lib/lemans/environments/daytona/shell.rb +1 -1
- data/lib/lemans/environments/daytona/snapshot_store.rb +11 -11
- data/lib/lemans/environments/daytona.rb +21 -35
- data/lib/lemans/result.rb +270 -0
- data/lib/lemans/runner/executor.rb +64 -0
- data/lib/lemans/runner/task.rb +62 -0
- data/lib/lemans/runner.rb +82 -0
- data/lib/lemans/store.rb +44 -0
- data/lib/lemans/stores/fs.rb +122 -0
- data/lib/lemans/task_definition.rb +194 -0
- data/lib/lemans/trial/patch.rb +76 -0
- data/lib/lemans/trial/setup.rb +66 -0
- data/lib/lemans/trial/snapshot.rb +57 -0
- data/lib/lemans/trial/verifier.rb +190 -0
- data/lib/lemans/trial.rb +113 -147
- data/lib/lemans/version.rb +1 -1
- data/lib/lemans.rb +3 -2
- data/lib/miniswen/trajectory.rb +2 -0
- metadata +58 -25
- data/lib/lemans/agents/base.rb +0 -30
- data/lib/lemans/bench.rb +0 -280
- data/lib/lemans/environments/base.rb +0 -55
- data/lib/lemans/network_policy.rb +0 -66
- data/lib/lemans/patch.rb +0 -70
- data/lib/lemans/restore_paths.rb +0 -21
- data/lib/lemans/results/aggregate.rb +0 -114
- data/lib/lemans/results/cost_source.rb +0 -13
- data/lib/lemans/results/outcome.rb +0 -36
- data/lib/lemans/results/report.rb +0 -149
- data/lib/lemans/results/sorting.rb +0 -24
- data/lib/lemans/results/tally.rb +0 -19
- data/lib/lemans/results/usage.rb +0 -24
- data/lib/lemans/run.rb +0 -152
- data/lib/lemans/setup.rb +0 -59
- data/lib/lemans/setup_files.rb +0 -36
- data/lib/lemans/snapshot.rb +0 -55
- data/lib/lemans/task.rb +0 -207
- data/lib/lemans/tree_digest.rb +0 -24
- data/lib/lemans/units.rb +0 -44
- data/lib/lemans/verifier.rb +0 -199
- /data/lib/lemans/{verifier → trial/verifier}/assets/eport-lemans.rb +0 -0
- /data/lib/lemans/{verifier → trial/verifier}/assets/lemans_minitest_reporter.rb +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9696bf1ad08f76e0a6faafd5651d67e1a98ca6e36e189ab41f71bdcb5f5f173e
|
|
4
|
+
data.tar.gz: 02e255c8829342789f9a057c1234a3a45bb2ca366db832caedb43d42dd0d7c5b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ccb12333aefc40203516d6e3cd98fdfadb2c96c6c159c4ea6acda851de502e5d9431ec8c69bd330e9b483a09bd6e5d93ef6d30e1dad23ba3f32485f1cf8a3283
|
|
7
|
+
data.tar.gz: 4548e86baeaefe091845ec3d16f91c8b08d54373c2d375f20e78b03e5a95c32f7506137dc02a0a218be02fd8fe8416e000566af91354cd7bf90301d248574c42
|
data/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Lemans is a harness for benchmarking coding agents, the Ruby way:
|
|
|
12
12
|
|
|
13
13
|
## Prerequisites
|
|
14
14
|
|
|
15
|
-
- Ruby 3.
|
|
15
|
+
- Ruby 3.4+ is required to run `lemans`
|
|
16
16
|
- Daytona account (API token)
|
|
17
17
|
- Some LLM provider/proxy credentials (e.g., OpenRouter)
|
|
18
18
|
|
|
@@ -36,7 +36,8 @@ my-bench/
|
|
|
36
36
|
└── tasks/
|
|
37
37
|
└── hello-world/
|
|
38
38
|
├── instruction.md # what the agent is asked to do; YAML frontmatter carries
|
|
39
|
-
│ # name, description, difficulty, tags, metadata
|
|
39
|
+
│ # name, description, difficulty, tags, metadata — plus the
|
|
40
|
+
│ # per-task overrides: setup, restore, verifier.setup
|
|
40
41
|
├── environment/Dockerfile # [optional] the sandbox image (you can use a shared image in bench.yml)
|
|
41
42
|
├── environment.patch # [optional] task setup patch: applied and resealed as a fresh git repo at setup
|
|
42
43
|
├── verification_test.rb # grades the result
|
|
@@ -48,6 +49,11 @@ A minimal example `bench.yml`:
|
|
|
48
49
|
```yaml
|
|
49
50
|
version: 1
|
|
50
51
|
|
|
52
|
+
# setup: # [optional] sandbox preparation, run before the agent starts
|
|
53
|
+
# files: [fixtures/seed.sql] # uploaded for the commands to consume, then wiped
|
|
54
|
+
# commands: [bin/sandbox-setup]
|
|
55
|
+
# A bare list is a commands shorthand: `setup: [bin/sandbox-setup]`
|
|
56
|
+
|
|
51
57
|
environment:
|
|
52
58
|
resources: { cpus: 2, memory: 2GB, storage: 5GB }
|
|
53
59
|
build_timeout: 10m
|
|
@@ -68,6 +74,7 @@ agent:
|
|
|
68
74
|
|
|
69
75
|
verifier:
|
|
70
76
|
timeout: 10m
|
|
77
|
+
# setup: [gem install debug] # [optional] grading prep (same files/commands form as setup:)
|
|
71
78
|
# preverify: ruby -report-lemans bin/rails test # [optional] a command that must pass first
|
|
72
79
|
# restore: [test, bin] # [optional] folders/files to restore before verification
|
|
73
80
|
```
|
|
@@ -129,6 +136,19 @@ A minimal task example—checking whether an agent can write "Hello, world" into
|
|
|
129
136
|
|
|
130
137
|
The task's contents go to `my-bench/tasks/hello-world`.
|
|
131
138
|
|
|
139
|
+
A task may extend the bench-wide preparation from its frontmatter — and only the preparation; resources, network, and the agent's budget are the frozen profile a task cannot touch:
|
|
140
|
+
|
|
141
|
+
```yaml
|
|
142
|
+
setup: # extra files/commands for this task's sandbox
|
|
143
|
+
files: [fixtures/data.csv] # relative to the task directory
|
|
144
|
+
commands: [bin/import]
|
|
145
|
+
restore: [test, config] # overrides verifier.restore for this task
|
|
146
|
+
verifier:
|
|
147
|
+
setup: [gem install debug] # extra grading prep on top of the bench-wide verifier.setup
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
An `environment.patch` next to `instruction.md` is always applied, declared or not.
|
|
151
|
+
|
|
132
152
|
### 3. Set credentials
|
|
133
153
|
|
|
134
154
|
```bash
|
|
@@ -164,19 +184,19 @@ ar-announce-once ✔✔
|
|
|
164
184
|
ar-archive-book-access ✔✔
|
|
165
185
|
⠧ 6/6 done · 0 in flight
|
|
166
186
|
|
|
167
|
-
task agent model reward outcome cost_usd steps tokens
|
|
168
|
-
ac-throttle-search miniswen-installed gpt-5.6-luna 1 completed 0.0112 11 118561 137.1
|
|
169
|
-
ac-throttle-search miniswen-installed gpt-5.6-luna 1 completed 0.0161 12 157111 144.1
|
|
170
|
-
ar-announce-once miniswen-installed gpt-5.6-luna 1 completed 0.0157 13 162432 186
|
|
171
|
-
ar-announce-once miniswen-installed gpt-5.6-luna 1 completed 0.0106 11 118691 117.8
|
|
172
|
-
ar-archive-book-access miniswen-installed gpt-5.6-luna 1 completed 0.0128 12 149867 136.2
|
|
173
|
-
ar-archive-book-access miniswen-installed gpt-5.6-luna 1 completed 0.0137 13 163943 149.6
|
|
187
|
+
task agent model reward outcome cost_usd steps tokens duration trial
|
|
188
|
+
ac-throttle-search miniswen-installed gpt-5.6-luna 1 completed 0.0112 11 118561 137.1 ac-throttle-search__j57Jfpt
|
|
189
|
+
ac-throttle-search miniswen-installed gpt-5.6-luna 1 completed 0.0161 12 157111 144.1 ac-throttle-search__ph2nfjN
|
|
190
|
+
ar-announce-once miniswen-installed gpt-5.6-luna 1 completed 0.0157 13 162432 186 ar-announce-once__Xi30ZmP
|
|
191
|
+
ar-announce-once miniswen-installed gpt-5.6-luna 1 completed 0.0106 11 118691 117.8 ar-announce-once__mo6a6zQ
|
|
192
|
+
ar-archive-book-access miniswen-installed gpt-5.6-luna 1 completed 0.0128 12 149867 136.2 ar-archive-book-access__oSRKzQc
|
|
193
|
+
ar-archive-book-access miniswen-installed gpt-5.6-luna 1 completed 0.0137 13 163943 149.6 ar-archive-book-access__XB5PRL3
|
|
174
194
|
6 trials: 6 scored, 0 invalid, 6 solved (100%) · $0.0801 · pass@2 3/3 tasks (100%)
|
|
175
195
|
```
|
|
176
196
|
|
|
177
197
|
`lemans run` runs all the tasks for the model defined in `bench.yml` and prints the report at the end. You can override the model(s) to use (`--model`), the number of attempts (`--attempts`), or select specific tasks by name (`--task=ac-throttle-search`, may be repeated).
|
|
178
198
|
|
|
179
|
-
Each trial writes a flat `runs/<model>/<task>__<id>/` directory: `result.json` (reward, outcome, usage, per-phase timings, tags, digests), `trajectory.json` (ATIF), `agent.patch` (the agent's work as one diff against the sealed baseline), `verifier.log`, and whatever the verifier left under `$LOGS` (`checks.json` included). `lemans run --resume` skips trials that already have a scored result for the same agent and model.
|
|
199
|
+
Each trial writes a flat `runs/<model>/<task>__<id>/` directory: `result.json` (reward, outcome, usage, per-phase timings, tags, digests), `trajectory.json` (ATIF), `agent.patch` (the agent's work as one diff against the sealed baseline), `verifier.log`, and whatever the verifier left under `$LOGS` (`checks.json` included). Every `result.json` is stamped with the `lemans_version` that wrote it, and newer lemans keeps reading runs produced by older releases. `lemans run --resume` skips trials that already have a scored result for the same agent and model.
|
|
180
200
|
|
|
181
201
|
You can also run `lemans report` with various flags to see aggregated results, e.g.:
|
|
182
202
|
|
data/exe/lemans-remote
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
#
|
|
26
26
|
# exe/lemans-remote status [--history] [--running | --complete]
|
|
27
27
|
# exe/lemans-remote pull-runs [RUN_ID ...] [--all] [--dry-run]
|
|
28
|
+
# exe/lemans-remote drop-orphans [--min-age 10m]
|
|
28
29
|
# exe/lemans-remote clobber RUN_ID ... | --all
|
|
29
30
|
# exe/lemans-remote snapshots
|
|
30
31
|
# exe/lemans-remote deprovision [--name SNAPSHOT]
|
|
@@ -698,12 +699,14 @@ module LemansRemote # :nodoc: all
|
|
|
698
699
|
end
|
|
699
700
|
|
|
700
701
|
class CLI < Thor
|
|
702
|
+
include Lemans::Config::Conversion
|
|
703
|
+
|
|
701
704
|
check_unknown_options!
|
|
702
705
|
|
|
703
706
|
def self.exit_on_failure? = true
|
|
704
707
|
|
|
705
|
-
class_option :cpus, type: :numeric, default:
|
|
706
|
-
class_option :memory, type: :numeric, default:
|
|
708
|
+
class_option :cpus, type: :numeric, default: 2, desc: "Orchestrator vCPUs (part of the snapshot identity)"
|
|
709
|
+
class_option :memory, type: :numeric, default: 4, desc: "Orchestrator memory, GiB"
|
|
707
710
|
class_option :disk, type: :numeric, default: 10, desc: "Orchestrator disk, GiB"
|
|
708
711
|
|
|
709
712
|
desc "provision", "Build the orchestrator snapshot (lemans #{Lemans::VERSION} preinstalled)"
|
|
@@ -737,6 +740,8 @@ module LemansRemote # :nodoc: all
|
|
|
737
740
|
option :keep, type: :boolean, default: false, desc: "Sync mode: leave the sandbox around for debugging"
|
|
738
741
|
option :runs_dir, default: "runs", desc: "Sync mode: local directory to sync the results into"
|
|
739
742
|
option :env, repeatable: true, desc: "Forward an extra host ENV variable by name"
|
|
743
|
+
option :concurrency, type: :numeric, default: 4, aliases: "-C",
|
|
744
|
+
desc: "Async mode: how many sandboxes to launch in parallel (1 to serialize)"
|
|
740
745
|
def run_bench
|
|
741
746
|
bench = Lemans::Bench.load(options[:bench])
|
|
742
747
|
tasks = options[:task] || []
|
|
@@ -749,6 +754,7 @@ module LemansRemote # :nodoc: all
|
|
|
749
754
|
attempts = options[:attempts].to_i
|
|
750
755
|
raise Thor::Error, "lemans-remote: --attempts must be at least 1" if attempts < 1
|
|
751
756
|
raise Thor::Error, "lemans-remote: --attempts needs async mode — drop --sync" if options[:sync] && attempts > 1
|
|
757
|
+
raise Thor::Error, "lemans-remote: --concurrency must be at least 1" if options[:concurrency].to_i < 1
|
|
752
758
|
|
|
753
759
|
models = options[:model] || []
|
|
754
760
|
batches = plan_batches(bench, tasks)
|
|
@@ -764,7 +770,7 @@ module LemansRemote # :nodoc: all
|
|
|
764
770
|
end
|
|
765
771
|
|
|
766
772
|
fanout = attempts > 1 ? "#{batches.size} task batch(es) × #{attempts} attempts" : "one per task"
|
|
767
|
-
say_status :fanout, "#{jobs.size} sandboxes, #{fanout}"
|
|
773
|
+
say_status :fanout, "#{jobs.size} sandboxes, #{fanout}, #{options[:concurrency].to_i} at a time"
|
|
768
774
|
failures = launch_batches(bench, jobs, models, provisioner)
|
|
769
775
|
say_status :detached, "`lemans-remote status` to watch, `lemans-remote pull-runs` to fetch results"
|
|
770
776
|
raise Thor::Error, "lemans-remote: failed to launch: #{failures.join("; ")}" if failures.any?
|
|
@@ -776,32 +782,27 @@ module LemansRemote # :nodoc: all
|
|
|
776
782
|
option :history, type: :boolean, default: false, desc: "Also read the vault manifests (spins a short-lived helper sandbox)"
|
|
777
783
|
option :running, type: :boolean, default: false, desc: "Only show runs still in flight"
|
|
778
784
|
option :complete, type: :boolean, default: false, desc: "Only show finished runs (success, failed, or stale)"
|
|
785
|
+
option :watch, type: :boolean, default: false, aliases: "-W", desc: "Redraw the table every 5 seconds until interrupted"
|
|
779
786
|
def status
|
|
780
|
-
|
|
781
|
-
if options[:history]
|
|
782
|
-
|
|
783
|
-
manifests = with_vault { |vault, sandbox| vault.manifests(sandbox) }
|
|
784
|
-
manifests.reject { seen.include?(_1["run_id"]) }.each do |manifest|
|
|
785
|
-
note = manifest["pulled_at"] ? "pulled" : "-"
|
|
786
|
-
rows << [manifest["run_id"], manifest["status"], manifest["started_at"] || "?", "gone", note]
|
|
787
|
-
end
|
|
788
|
-
end
|
|
789
|
-
|
|
790
|
-
filtered = options[:running] ^ options[:complete]
|
|
791
|
-
rows.select! { (_1[1] == "running") == options[:running] } if filtered
|
|
787
|
+
no_watch_history = "lemans-remote: --watch cannot be combined with --history (each refresh would spin a helper sandbox)"
|
|
788
|
+
raise Thor::Error, no_watch_history if options[:watch] && options[:history]
|
|
789
|
+
return print_status unless options[:watch]
|
|
792
790
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
791
|
+
loop do
|
|
792
|
+
print "\e[H\e[2J"
|
|
793
|
+
say "lemans-remote status — #{Time.now.strftime("%H:%M:%S")}, refreshing every 5s (^C to stop)"
|
|
794
|
+
begin
|
|
795
|
+
unless print_status
|
|
796
|
+
say_status :done, "no running sandboxes — stopping watch", :green
|
|
797
|
+
break
|
|
798
|
+
end
|
|
799
|
+
rescue StandardError => e
|
|
800
|
+
say_status :error, e.message, :red
|
|
798
801
|
end
|
|
799
|
-
|
|
802
|
+
sleep 5
|
|
800
803
|
end
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
rows.reverse!
|
|
804
|
-
print_table([%w[run status started sandbox id]] + rows)
|
|
804
|
+
rescue Interrupt
|
|
805
|
+
say ""
|
|
805
806
|
rescue Lemans::ConfigError, RuntimeError => e
|
|
806
807
|
raise Thor::Error, "lemans-remote: #{e.message}"
|
|
807
808
|
end
|
|
@@ -893,6 +894,32 @@ module LemansRemote # :nodoc: all
|
|
|
893
894
|
raise Thor::Error, "lemans-remote: #{e.message}"
|
|
894
895
|
end
|
|
895
896
|
|
|
897
|
+
map "drop-orphans" => :drop_orphans
|
|
898
|
+
desc "drop-orphans", "Delete run sandboxes whose launch died mid-flight (forever running, nothing uploaded)"
|
|
899
|
+
option :min_age, default: "10m", desc: "Leave younger runs alone — they may still be uploading"
|
|
900
|
+
option :force, type: :boolean, default: false, aliases: "-f", desc: "Skip the confirmation"
|
|
901
|
+
def drop_orphans
|
|
902
|
+
cutoff = Time.now.utc - seconds!(options[:min_age])
|
|
903
|
+
candidates = Fleet.live.select do |row|
|
|
904
|
+
row.status == "running" && row.started_at && Time.parse(row.started_at) <= cutoff
|
|
905
|
+
end
|
|
906
|
+
orphans = candidates.select { orphan?(_1.sandbox) }
|
|
907
|
+
if orphans.empty?
|
|
908
|
+
say_status :clean, "no orphaned run sandboxes", :green
|
|
909
|
+
return
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
orphans.each { say_status :orphan, "#{_1.run_id} (#{_1.sandbox.id}, started #{_1.started_at})", :yellow }
|
|
913
|
+
return unless options[:force] || yes?("Delete #{orphans.size} orphaned sandbox(es)? [y/N]")
|
|
914
|
+
|
|
915
|
+
orphans.each do |row|
|
|
916
|
+
safe_delete_sandbox(row.sandbox)
|
|
917
|
+
say_status :dropped, row.run_id, :green
|
|
918
|
+
end
|
|
919
|
+
rescue Lemans::ConfigError, RuntimeError => e
|
|
920
|
+
raise Thor::Error, "lemans-remote: #{e.message}"
|
|
921
|
+
end
|
|
922
|
+
|
|
896
923
|
desc "snapshots", "List lemans-remote orchestrator snapshots"
|
|
897
924
|
def snapshots
|
|
898
925
|
current = build_provisioner.name
|
|
@@ -934,10 +961,39 @@ module LemansRemote # :nodoc: all
|
|
|
934
961
|
say Lemans::VERSION
|
|
935
962
|
end
|
|
936
963
|
|
|
937
|
-
FANOUT_THREADS = 4
|
|
938
|
-
|
|
939
964
|
private
|
|
940
965
|
|
|
966
|
+
def print_status
|
|
967
|
+
rows = Fleet.rows.map { [_1.run_id, _1.status, _1.started_at || "?", _1.state, _1.sandbox.id] }
|
|
968
|
+
if options[:history]
|
|
969
|
+
seen = rows.map(&:first)
|
|
970
|
+
manifests = with_vault { |vault, sandbox| vault.manifests(sandbox) }
|
|
971
|
+
manifests.reject { seen.include?(_1["run_id"]) }.each do |manifest|
|
|
972
|
+
note = manifest["pulled_at"] ? "pulled" : "-"
|
|
973
|
+
rows << [manifest["run_id"], manifest["status"], manifest["started_at"] || "?", "gone", note]
|
|
974
|
+
end
|
|
975
|
+
end
|
|
976
|
+
|
|
977
|
+
any_running = rows.any? { _1[1] == "running" }
|
|
978
|
+
|
|
979
|
+
filtered = options[:running] ^ options[:complete]
|
|
980
|
+
rows.select! { (_1[1] == "running") == options[:running] } if filtered
|
|
981
|
+
|
|
982
|
+
if rows.empty?
|
|
983
|
+
if filtered
|
|
984
|
+
say "no #{options[:running] ? "running" : "complete"} lemans-remote runs"
|
|
985
|
+
else
|
|
986
|
+
say "no lemans-remote runs#{" — try --history for completed ones" unless options[:history]}"
|
|
987
|
+
end
|
|
988
|
+
return any_running
|
|
989
|
+
end
|
|
990
|
+
|
|
991
|
+
rows.sort_by! { _1[2].to_s }
|
|
992
|
+
rows.reverse!
|
|
993
|
+
print_table([%w[run status started sandbox id]] + rows)
|
|
994
|
+
any_running
|
|
995
|
+
end
|
|
996
|
+
|
|
941
997
|
def build_provisioner
|
|
942
998
|
Provisioner.new(cpus: options[:cpus].to_i, memory_gb: options[:memory].to_i, disk_gb: options[:disk].to_i)
|
|
943
999
|
end
|
|
@@ -958,7 +1014,7 @@ module LemansRemote # :nodoc: all
|
|
|
958
1014
|
models: models,
|
|
959
1015
|
extra_args: options[:args].strip,
|
|
960
1016
|
extra_env: options[:env] || [],
|
|
961
|
-
timeout_sec:
|
|
1017
|
+
timeout_sec: seconds!(options[:timeout]).to_i,
|
|
962
1018
|
runs_dir: options[:runs_dir],
|
|
963
1019
|
keep: options[:keep],
|
|
964
1020
|
sync: options[:sync],
|
|
@@ -971,7 +1027,7 @@ module LemansRemote # :nodoc: all
|
|
|
971
1027
|
jobs.each { queue << _1 }
|
|
972
1028
|
failures = Queue.new
|
|
973
1029
|
|
|
974
|
-
threads = [
|
|
1030
|
+
threads = [options[:concurrency].to_i, jobs.size].min.times.map do
|
|
975
1031
|
Thread.new do
|
|
976
1032
|
loop do
|
|
977
1033
|
batch, attempt =
|
|
@@ -1038,6 +1094,15 @@ module LemansRemote # :nodoc: all
|
|
|
1038
1094
|
rescue StandardError => e
|
|
1039
1095
|
warn "lemans-remote: could not delete sandbox #{sandbox.id}: #{e.message}"
|
|
1040
1096
|
end
|
|
1097
|
+
|
|
1098
|
+
# Every launched run extracts the bench into /task within minutes of
|
|
1099
|
+
# creation (async ones also upload the wrapper); a sandbox whose launcher
|
|
1100
|
+
# process died mid-flight has neither, forever.
|
|
1101
|
+
def orphan?(sandbox)
|
|
1102
|
+
!sandbox.process.exec(command: "test -d #{REMOTE_BENCH_DIR} -o -f #{REMOTE_WRAPPER}", timeout: 60).exit_code.zero?
|
|
1103
|
+
rescue StandardError
|
|
1104
|
+
false
|
|
1105
|
+
end
|
|
1041
1106
|
end
|
|
1042
1107
|
end
|
|
1043
1108
|
|
data/lib/lemans/agent.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lemans
|
|
4
|
+
# What the harness asks of an agent: install yourself, then work on the
|
|
5
|
+
# task. The name-to-class registry lives on the Agents module.
|
|
6
|
+
class Agent
|
|
7
|
+
# What one run of an agent produced. Outcome and usage are the core
|
|
8
|
+
# Result's parts; trajectory, the raw self-reported result, and the error
|
|
9
|
+
# are optional — the trial persists them, agents only carry them.
|
|
10
|
+
Response = Data.define(:outcome, :usage, :trajectory, :error, :raw_result) do
|
|
11
|
+
def initialize(outcome: nil, usage: nil, trajectory: nil, error: nil, raw_result: nil) = super
|
|
12
|
+
|
|
13
|
+
def error? = !error.nil?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
attr_reader :profile, :model
|
|
17
|
+
|
|
18
|
+
def initialize(profile:, model: nil)
|
|
19
|
+
@profile = profile
|
|
20
|
+
|
|
21
|
+
@model = model || profile.model
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def name = self.class::NAME
|
|
25
|
+
|
|
26
|
+
# Run before the agent phase's network policy narrows, so an agent that
|
|
27
|
+
# pulls its own runtime can still reach a package index.
|
|
28
|
+
def install(_task, _environment) = nil
|
|
29
|
+
|
|
30
|
+
# Run the task.
|
|
31
|
+
def run(task, environment)
|
|
32
|
+
raise NotImplementedError
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def timeout = profile.timeout
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -7,9 +7,8 @@ module Lemans
|
|
|
7
7
|
module Agents
|
|
8
8
|
# The harness adapter for Miniswen::Agent. The loop runs harness-side, so
|
|
9
9
|
# there is nothing to install and no model API in the sandbox allowlist.
|
|
10
|
-
class Miniswen <
|
|
10
|
+
class Miniswen < Agent
|
|
11
11
|
NAME = "miniswen"
|
|
12
|
-
TRAJECTORY_FILENAME = "trajectory.json"
|
|
13
12
|
|
|
14
13
|
OUTCOME_FOR_STATUS = {
|
|
15
14
|
submitted: :completed,
|
|
@@ -22,22 +21,25 @@ module Lemans
|
|
|
22
21
|
cost_limit: :cost_ceiling_reached
|
|
23
22
|
}.freeze
|
|
24
23
|
|
|
25
|
-
def
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
def run(task, environment)
|
|
25
|
+
run_result = obtain_result(task, environment)
|
|
26
|
+
trajectory = trajectory_for(run_result)
|
|
28
27
|
|
|
29
|
-
|
|
28
|
+
# A failed model call is still an answer: the trial saves the
|
|
29
|
+
# trajectory as evidence before failing.
|
|
30
|
+
return Response.new(trajectory:, raw_result:, error: run_result.error) if run_result.status == :error
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
outcome:
|
|
33
|
-
usage: usage_for(
|
|
34
|
-
trajectory
|
|
32
|
+
Response.new(
|
|
33
|
+
outcome: Result::Outcome.new(OUTCOME_FOR_STATUS.fetch(run_result.status), detail_for(run_result)),
|
|
34
|
+
usage: usage_for(run_result),
|
|
35
|
+
trajectory:,
|
|
36
|
+
raw_result:
|
|
35
37
|
)
|
|
36
38
|
end
|
|
37
39
|
|
|
38
40
|
private
|
|
39
41
|
|
|
40
|
-
def obtain_result(
|
|
42
|
+
def obtain_result(task, environment)
|
|
41
43
|
agent = agent_for(environment)
|
|
42
44
|
begin
|
|
43
45
|
agent.run(task.instruction)
|
|
@@ -46,6 +48,8 @@ module Lemans
|
|
|
46
48
|
end
|
|
47
49
|
end
|
|
48
50
|
|
|
51
|
+
def raw_result = nil
|
|
52
|
+
|
|
49
53
|
def agent_for(environment)
|
|
50
54
|
raise ConfigError, "miniswen needs a model to drive" if model.to_s.empty?
|
|
51
55
|
|
|
@@ -53,9 +57,9 @@ module Lemans
|
|
|
53
57
|
model: model.to_s,
|
|
54
58
|
environment: environment,
|
|
55
59
|
max_steps: profile.step_limit,
|
|
56
|
-
max_time: profile.
|
|
60
|
+
max_time: profile.timeout,
|
|
57
61
|
max_cost: profile.cost_limit,
|
|
58
|
-
exec_timeout: profile.
|
|
62
|
+
exec_timeout: profile.exec_timeout
|
|
59
63
|
)
|
|
60
64
|
end
|
|
61
65
|
|
|
@@ -76,7 +80,7 @@ module Lemans
|
|
|
76
80
|
}
|
|
77
81
|
# Only a run that never called the model spent nothing; a zero count
|
|
78
82
|
# on a run that did is missing data, not a free run.
|
|
79
|
-
return
|
|
83
|
+
return Result::Usage.zero if result.steps.zero?
|
|
80
84
|
|
|
81
85
|
if result.cost_usd.nil?
|
|
82
86
|
raise ::Miniswen::AccountingError,
|
|
@@ -84,23 +88,22 @@ module Lemans
|
|
|
84
88
|
"#{result.output_tokens} output tokens cannot be reported as $0.00"
|
|
85
89
|
end
|
|
86
90
|
|
|
87
|
-
|
|
91
|
+
Result::Usage.new(
|
|
92
|
+
**totals,
|
|
93
|
+
cost_usd: result.cost_usd,
|
|
94
|
+
# FIXME: need a better way to map Miniswen's cost source to Lemans'
|
|
95
|
+
cost_source: Result::CostSource.new(**result.cost_source.to_h)
|
|
96
|
+
)
|
|
88
97
|
end
|
|
89
98
|
|
|
90
|
-
def
|
|
91
|
-
|
|
99
|
+
def trajectory_for(result)
|
|
100
|
+
::Miniswen::Trajectory.from(
|
|
92
101
|
result,
|
|
93
102
|
model: model,
|
|
94
|
-
session_id: session_id_for(logs_dir),
|
|
95
103
|
agent: { name: name, version: VERSION, extra: agent_extra }
|
|
96
104
|
)
|
|
97
|
-
path = logs_dir.join(TRAJECTORY_FILENAME)
|
|
98
|
-
path.write(JSON.pretty_generate(trajectory.to_atif))
|
|
99
|
-
path
|
|
100
105
|
end
|
|
101
106
|
|
|
102
|
-
def session_id_for(logs_dir) = Pathname(logs_dir).basename.to_s
|
|
103
|
-
|
|
104
107
|
# What the trajectory cannot be read without: the prompts the model saw
|
|
105
108
|
# and the budget it worked under
|
|
106
109
|
def agent_extra
|
|
@@ -109,8 +112,8 @@ module Lemans
|
|
|
109
112
|
instance_template: ::Miniswen::Agent::INSTANCE_TEMPLATE,
|
|
110
113
|
step_limit: profile.step_limit,
|
|
111
114
|
cost_limit: profile.cost_limit,
|
|
112
|
-
wall_time_limit_seconds: profile.
|
|
113
|
-
exec_timeout_seconds: profile.
|
|
115
|
+
wall_time_limit_seconds: profile.timeout,
|
|
116
|
+
exec_timeout_seconds: profile.exec_timeout,
|
|
114
117
|
max_consecutive_format_errors: ::Miniswen::Agent::MAX_CONSECUTIVE_FORMAT_ERRORS
|
|
115
118
|
}.compact }
|
|
116
119
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "shellwords"
|
|
5
|
+
require "tempfile"
|
|
5
6
|
|
|
6
7
|
module Lemans
|
|
7
8
|
module Agents
|
|
@@ -12,13 +13,12 @@ module Lemans
|
|
|
12
13
|
class MiniswenInstalled < Miniswen
|
|
13
14
|
NAME = "miniswen-installed"
|
|
14
15
|
RESULTS_PATH = "/tmp/lemans-miniswen.result.json"
|
|
15
|
-
RESULT_FILENAME = "miniswen.result.json"
|
|
16
16
|
INSTALL_TIMEOUT_SEC = 300
|
|
17
17
|
# The CLI enforces max-time itself; the slack only covers process
|
|
18
18
|
# startup, so the results file exists before the outer exec expires.
|
|
19
19
|
EXEC_SLACK_SEC = 60
|
|
20
20
|
|
|
21
|
-
def install(
|
|
21
|
+
def install(_task, environment)
|
|
22
22
|
environment.exec!(
|
|
23
23
|
"command -v miniswen >/dev/null 2>&1 || gem install miniswen -v #{::Miniswen::VERSION} --no-document",
|
|
24
24
|
timeout: INSTALL_TIMEOUT_SEC
|
|
@@ -30,14 +30,16 @@ module Lemans
|
|
|
30
30
|
|
|
31
31
|
# An in-sandbox run self-reports: everything but the verifier's reward
|
|
32
32
|
# comes from a file the sandbox wrote.
|
|
33
|
-
def obtain_result(
|
|
34
|
-
run = environment.exec(command_for(task), timeout: profile.
|
|
33
|
+
def obtain_result(task, environment)
|
|
34
|
+
run = environment.exec(command_for(task), timeout: profile.timeout + EXEC_SLACK_SEC,
|
|
35
35
|
env: provider_env(environment))
|
|
36
36
|
|
|
37
|
-
local = logs_dir.join(RESULT_FILENAME)
|
|
38
37
|
begin
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
Tempfile.create(%w[miniswen .result.json]) do |file|
|
|
39
|
+
environment.download(RESULTS_PATH, file.path)
|
|
40
|
+
@raw_result = File.read(file.path)
|
|
41
|
+
::Miniswen::Agent::Result.from_h(JSON.parse(@raw_result))
|
|
42
|
+
end
|
|
41
43
|
rescue StandardError => e
|
|
42
44
|
raise InfrastructureError,
|
|
43
45
|
"miniswen-installed: no usable result file (exit #{run.exit_code}, #{e.message}): " \
|
|
@@ -45,6 +47,8 @@ module Lemans
|
|
|
45
47
|
end
|
|
46
48
|
end
|
|
47
49
|
|
|
50
|
+
attr_reader :raw_result
|
|
51
|
+
|
|
48
52
|
# A missing credential fails the run before the sandbox executes
|
|
49
53
|
# anything: it is the operator's configuration to fix, not a trial result.
|
|
50
54
|
def provider_env(environment)
|
|
@@ -57,8 +61,8 @@ module Lemans
|
|
|
57
61
|
argv = ["miniswen", "-q", "--no-refresh-registry",
|
|
58
62
|
"-m", model.to_s, "-p", task.instruction,
|
|
59
63
|
"--results-path", RESULTS_PATH,
|
|
60
|
-
"--max-steps", profile.step_limit, "--max-time", profile.
|
|
61
|
-
"--exec-timeout", profile.
|
|
64
|
+
"--max-steps", profile.step_limit, "--max-time", profile.timeout.to_i,
|
|
65
|
+
"--exec-timeout", profile.exec_timeout.to_i]
|
|
62
66
|
argv += ["--max-cost", profile.cost_limit.to_i] if profile.cost_limit
|
|
63
67
|
argv.map { Shellwords.escape(_1.to_s) }.join(" ")
|
|
64
68
|
end
|
data/lib/lemans/agents/nop.rb
CHANGED
|
@@ -4,11 +4,11 @@ module Lemans
|
|
|
4
4
|
module Agents
|
|
5
5
|
# Does nothing, on purpose: how a task proves its verifier rejects an
|
|
6
6
|
# untouched tree.
|
|
7
|
-
class Nop <
|
|
7
|
+
class Nop < Agent
|
|
8
8
|
NAME = "nop"
|
|
9
9
|
|
|
10
|
-
def
|
|
11
|
-
|
|
10
|
+
def run(_task, _environment)
|
|
11
|
+
Response.new(outcome: Result::Outcome.new(:completed), usage: Result::Usage.zero)
|
|
12
12
|
end
|
|
13
13
|
end
|
|
14
14
|
end
|
data/lib/lemans/agents/oracle.rb
CHANGED
|
@@ -6,26 +6,26 @@ module Lemans
|
|
|
6
6
|
module Agents
|
|
7
7
|
# Runs the task's own solution instead of a model. A task whose oracle
|
|
8
8
|
# does not score full marks is broken, not hard.
|
|
9
|
-
class Oracle <
|
|
9
|
+
class Oracle < Agent
|
|
10
10
|
NAME = "oracle"
|
|
11
11
|
REMOTE_DIR = "/solution"
|
|
12
12
|
SOLVE = "solve"
|
|
13
13
|
ENTRYPOINT = "solve.sh"
|
|
14
14
|
PATCH = "solution.patch"
|
|
15
15
|
|
|
16
|
-
def
|
|
16
|
+
def run(task, environment)
|
|
17
17
|
raise ConfigError, "#{task.name}: no solution/ to run — the oracle has nothing to prove" unless task.solution?
|
|
18
18
|
|
|
19
19
|
upload_solution(environment, task)
|
|
20
|
-
|
|
20
|
+
outcome = environment.exec(command_for(task), timeout:)
|
|
21
21
|
|
|
22
|
-
unless
|
|
22
|
+
unless outcome.success?
|
|
23
23
|
raise InfrastructureError,
|
|
24
|
-
"#{task.name}: the solution itself failed (exit #{
|
|
25
|
-
"#{
|
|
24
|
+
"#{task.name}: the solution itself failed (exit #{outcome.exit_code}): " \
|
|
25
|
+
"#{outcome.output.to_s[0, 500]}"
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
Response.new(outcome: Result::Outcome.new(:completed), usage: Result::Usage.zero)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
private
|
|
@@ -40,7 +40,7 @@ module Lemans
|
|
|
40
40
|
|
|
41
41
|
raise ConfigError, "#{task.name}: the solution ships neither #{SOLVE}, #{ENTRYPOINT} nor #{PATCH}" unless shipped.include?(PATCH)
|
|
42
42
|
|
|
43
|
-
"cd #{Shellwords.escape(task.
|
|
43
|
+
"cd #{Shellwords.escape(task.environment.workdir)} && git apply --binary --whitespace=nowarn #{REMOTE_DIR}/#{PATCH}"
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
def upload_solution(environment, task)
|