lemans 1.1.0 → 1.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/CHANGELOG.md +15 -0
- data/README.md +14 -4
- data/exe/lemans-remote +22 -10
- data/lib/lemans/agents/miniswen.rb +2 -2
- data/lib/lemans/agents/oracle.rb +13 -7
- data/lib/lemans/cli/progress_reporter.rb +6 -1
- data/lib/lemans/cli/regrade.rb +193 -0
- data/lib/lemans/cli/report/aggregate.rb +18 -11
- data/lib/lemans/cli/report.rb +25 -8
- data/lib/lemans/cli/templates/bench/README.md +7 -2
- data/lib/lemans/cli/templates/bench/tasks/example-task/instruction.md +20 -9
- data/lib/lemans/cli/templates/bench/tasks/example-task/solution.1.patch +17 -0
- data/lib/lemans/cli/templates/bench/tasks/example-task/solution.2.patch +17 -0
- data/lib/lemans/cli/templates/bench/tasks/example-task/verification_test.1.rb +31 -0
- data/lib/lemans/cli/templates/bench/tasks/example-task/verification_test.rb +1 -1
- data/lib/lemans/cli.rb +38 -1
- data/lib/lemans/config/agent.rb +12 -0
- data/lib/lemans/config/environment.rb +31 -2
- data/lib/lemans/config/network_policy.rb +2 -0
- data/lib/lemans/config/setup.rb +6 -1
- data/lib/lemans/config/verifier.rb +18 -7
- data/lib/lemans/config.rb +45 -10
- data/lib/lemans/environment.rb +6 -3
- data/lib/lemans/environments/daytona/retries.rb +5 -3
- data/lib/lemans/environments/daytona/shell.rb +11 -1
- data/lib/lemans/environments/daytona.rb +16 -6
- data/lib/lemans/environments/docker.rb +3 -3
- data/lib/lemans/ext/deep_merge.rb +13 -0
- data/lib/lemans/result.rb +57 -7
- data/lib/lemans/runner.rb +1 -1
- data/lib/lemans/store.rb +7 -1
- data/lib/lemans/stores/fs.rb +9 -3
- data/lib/lemans/task_definition.rb +158 -10
- data/lib/lemans/trial/patch.rb +46 -8
- data/lib/lemans/trial/verifier/assets/eport-lemans.rb +6 -0
- data/lib/lemans/trial/verifier/assets/lemans_minitest_reporter.rb +44 -3
- data/lib/lemans/trial/verifier.rb +30 -3
- data/lib/lemans/trial.rb +89 -33
- data/lib/lemans/version.rb +1 -1
- data/lib/miniswen/agent.rb +21 -2
- data/lib/miniswen/version.rb +1 -1
- metadata +6 -2
- data/lib/lemans/cli/templates/bench/tasks/example-task/solution.patch +0 -7
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
3
4
|
require "pathname"
|
|
4
5
|
require "shellwords"
|
|
5
6
|
require "tmpdir"
|
|
@@ -9,7 +10,7 @@ module Lemans
|
|
|
9
10
|
# Verifies a trial in the sandbox the agent worked in, after Trial has closed
|
|
10
11
|
# its network. The tests are uploaded fresh at verification time, never before.
|
|
11
12
|
class Verifier
|
|
12
|
-
Verification = Data.define(:reward, :logs)
|
|
13
|
+
Verification = Data.define(:reward, :credit, :logs)
|
|
13
14
|
|
|
14
15
|
REWARD_RANGE = (0.0..1.0)
|
|
15
16
|
|
|
@@ -40,7 +41,7 @@ module Lemans
|
|
|
40
41
|
prepare_env!
|
|
41
42
|
|
|
42
43
|
# A baseline the agent made unrestorable is a verdict, not an error.
|
|
43
|
-
return Verification.new(reward: 0.0, logs: TAMPERED) unless snapshot.restore!
|
|
44
|
+
return Verification.new(reward: 0.0, credit: 0.0, logs: TAMPERED) unless snapshot.restore!
|
|
44
45
|
|
|
45
46
|
verification = run_tests!
|
|
46
47
|
|
|
@@ -98,7 +99,8 @@ module Lemans
|
|
|
98
99
|
|
|
99
100
|
result = environment.exec(command, timeout:, env:)
|
|
100
101
|
|
|
101
|
-
|
|
102
|
+
reward = read_reward(result)
|
|
103
|
+
Verification.new(reward:, credit: read_credit(reward), logs: result.output.to_s)
|
|
102
104
|
end
|
|
103
105
|
|
|
104
106
|
def verifier_script
|
|
@@ -124,6 +126,31 @@ module Lemans
|
|
|
124
126
|
value
|
|
125
127
|
end
|
|
126
128
|
|
|
129
|
+
def read_credit(reward)
|
|
130
|
+
path = File.join(task.verifier.logs_dir, "checks.json")
|
|
131
|
+
return reward unless environment.exec("test -e #{Shellwords.escape(path)}").success?
|
|
132
|
+
|
|
133
|
+
result = environment.exec("cat #{Shellwords.escape(path)}")
|
|
134
|
+
raise VerifierError, "could not read #{path}: #{result.output.to_s[0, 500]}" unless result.success?
|
|
135
|
+
|
|
136
|
+
checks = begin
|
|
137
|
+
JSON.parse(result.output.to_s)
|
|
138
|
+
rescue JSON::ParserError => e
|
|
139
|
+
raise VerifierError, "#{path} is not JSON: #{e.message[0, 500]}"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
grading = checks["grading"]
|
|
143
|
+
return reward unless grading && (base_credit = grading["base_credit"])
|
|
144
|
+
return 0.0 if reward.zero?
|
|
145
|
+
|
|
146
|
+
points = grading.fetch("points", {})
|
|
147
|
+
total = points.values.sum
|
|
148
|
+
return reward if total.zero?
|
|
149
|
+
|
|
150
|
+
passed = points.sum { |check, value| checks.dig("checks", check) == "pass" ? value : 0 }
|
|
151
|
+
(base_credit + (1 - base_credit) * (passed.to_f / total)).round(2)
|
|
152
|
+
end
|
|
153
|
+
|
|
127
154
|
def reward_from_exit(command_result)
|
|
128
155
|
case command_result.exit_code
|
|
129
156
|
when 0 then 1.0
|
data/lib/lemans/trial.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "pathname"
|
|
5
|
+
require "shellwords"
|
|
5
6
|
require "time"
|
|
6
7
|
|
|
7
8
|
module Lemans
|
|
@@ -11,7 +12,7 @@ module Lemans
|
|
|
11
12
|
class Trial
|
|
12
13
|
attr_reader :task, :config, :model, :agent_name, :environment, :result
|
|
13
14
|
|
|
14
|
-
private attr_reader :agent, :store, :snapshot, :patch
|
|
15
|
+
private attr_reader :agent, :store, :snapshot, :patch, :current_step_index
|
|
15
16
|
|
|
16
17
|
def initialize(task, model = nil, result: nil, store: nil, agent: nil, environment: nil)
|
|
17
18
|
@task = task
|
|
@@ -34,6 +35,7 @@ module Lemans
|
|
|
34
35
|
resources: task.environment.resources,
|
|
35
36
|
network: task.environment.network,
|
|
36
37
|
build_timeout: task.environment.build_timeout,
|
|
38
|
+
ttl: sandbox_ttl,
|
|
37
39
|
labels: {
|
|
38
40
|
"lemans.task" => task.name,
|
|
39
41
|
"lemans.trial" => self.result.id,
|
|
@@ -44,6 +46,7 @@ module Lemans
|
|
|
44
46
|
|
|
45
47
|
@snapshot = nil
|
|
46
48
|
@patch = nil
|
|
49
|
+
@current_step_index = nil
|
|
47
50
|
end
|
|
48
51
|
|
|
49
52
|
def run
|
|
@@ -67,42 +70,60 @@ module Lemans
|
|
|
67
70
|
environment.switch_network_policy!(config.agent.environment.network)
|
|
68
71
|
end
|
|
69
72
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
agent
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
each_step do |step_task|
|
|
74
|
+
response =
|
|
75
|
+
phase(:agent) do
|
|
76
|
+
agent.run(step_task, environment)
|
|
77
|
+
rescue InfrastructureError, ::Miniswen::InfrastructureError => e
|
|
78
|
+
# Mark the failure here, where the agent phase is still known
|
|
79
|
+
result.failed!(:agent_error, e.message)
|
|
80
|
+
raise
|
|
81
|
+
end
|
|
78
82
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
# Whatever the agent brought back is evidence, a failed run's included
|
|
84
|
+
save_trajectory!(response.trajectory)
|
|
85
|
+
store&.save_artifact(result, response.raw_result, path: with_step_index("agent.result.json")) if response.raw_result
|
|
82
86
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
if response.error?
|
|
88
|
+
result.failed!(:agent_error, response.error)
|
|
89
|
+
return result
|
|
90
|
+
end
|
|
87
91
|
|
|
88
|
-
|
|
92
|
+
if task.multistep?
|
|
93
|
+
result.step_completed!(response.outcome, response.usage, duration: result.phases.last.duration)
|
|
94
|
+
else
|
|
95
|
+
result.completed!(response.outcome, response.usage)
|
|
96
|
+
end
|
|
89
97
|
|
|
90
|
-
|
|
98
|
+
check_cost_limit!
|
|
91
99
|
|
|
92
|
-
|
|
100
|
+
patch.collect!(result, store, path: with_step_index("agent.patch")) if store
|
|
101
|
+
if step_task.final_step?
|
|
102
|
+
patch.compile!(result, store) if task.multistep? && store
|
|
103
|
+
# Don't index the final verification
|
|
104
|
+
@current_step_index = nil
|
|
105
|
+
else
|
|
106
|
+
patch.savepoint!
|
|
107
|
+
end
|
|
93
108
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
109
|
+
if result.scored? && step_task.verifiable?
|
|
110
|
+
phase(:verifier) do
|
|
111
|
+
# The sandbox is sealed before the tests arrive
|
|
112
|
+
environment.switch_network_policy!(Config::NetworkPolicy.new("none"))
|
|
98
113
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
114
|
+
verification = Verifier.new(step_task, environment, snapshot).verify! do |evidence, path|
|
|
115
|
+
store&.save_artifact(result, evidence, path: with_step_index(path))
|
|
116
|
+
end
|
|
102
117
|
|
|
103
|
-
|
|
118
|
+
store&.save_artifact(result, verification.logs, path: with_step_index("verifier.log"))
|
|
104
119
|
|
|
105
|
-
|
|
120
|
+
if step_task.final_step?
|
|
121
|
+
result.graded!(verification.reward, credit: verification.credit)
|
|
122
|
+
elsif verification.reward.zero?
|
|
123
|
+
result.graded!(0.0)
|
|
124
|
+
throw :halt
|
|
125
|
+
end
|
|
126
|
+
end
|
|
106
127
|
end
|
|
107
128
|
end
|
|
108
129
|
|
|
@@ -125,11 +146,46 @@ module Lemans
|
|
|
125
146
|
|
|
126
147
|
private
|
|
127
148
|
|
|
128
|
-
def save_trajectory(trajectory)
|
|
149
|
+
def save_trajectory!(trajectory)
|
|
129
150
|
return unless trajectory && store
|
|
130
151
|
|
|
131
|
-
|
|
132
|
-
|
|
152
|
+
path = with_step_index("trajectory.json")
|
|
153
|
+
session_id = with_step_index(result.id)
|
|
154
|
+
|
|
155
|
+
trajectory.session_id = session_id
|
|
156
|
+
store.save_artifact(result, JSON.pretty_generate(trajectory.to_atif), path:)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def each_step
|
|
160
|
+
return yield task unless task.multistep?
|
|
161
|
+
|
|
162
|
+
catch(:halt) do
|
|
163
|
+
1.upto(task.steps) do |index|
|
|
164
|
+
resume_agent! if index > 1
|
|
165
|
+
@current_step_index = index
|
|
166
|
+
yield task.for_step(index)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def resume_agent!
|
|
172
|
+
patch.restore!
|
|
173
|
+
environment.exec!("rm -rf #{Verifier::TESTS_DIR} #{Shellwords.escape(task.verifier.logs_dir)}")
|
|
174
|
+
environment.switch_network_policy!(config.agent.environment.network)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def with_step_index(path)
|
|
178
|
+
return path unless current_step_index
|
|
179
|
+
|
|
180
|
+
*pre, last = path.to_s.split(".")
|
|
181
|
+
return "#{last}.#{current_step_index}" if pre.empty?
|
|
182
|
+
|
|
183
|
+
[ *pre, current_step_index, last ].join(".")
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def sandbox_ttl
|
|
187
|
+
task.environment.sandbox_ttl ||
|
|
188
|
+
[ 3600, task.environment.build_timeout + task.steps * (config.agent.timeout + task.verifier.timeout) + 600 ].max
|
|
133
189
|
end
|
|
134
190
|
|
|
135
191
|
def check_cost_limit!
|
|
@@ -144,10 +200,10 @@ module Lemans
|
|
|
144
200
|
end
|
|
145
201
|
|
|
146
202
|
def phase(name)
|
|
147
|
-
result.phase_started(name)
|
|
203
|
+
result.phase_started(with_step_index(name).to_sym)
|
|
148
204
|
yield
|
|
149
205
|
ensure
|
|
150
|
-
result.phase_finished(name)
|
|
206
|
+
result.phase_finished(with_step_index(name).to_sym)
|
|
151
207
|
end
|
|
152
208
|
end
|
|
153
209
|
end
|
data/lib/lemans/version.rb
CHANGED
data/lib/miniswen/agent.rb
CHANGED
|
@@ -27,6 +27,11 @@ module Miniswen
|
|
|
27
27
|
# The breakpoint marker Anthropic reads, shaped the way OpenRouter forwards it.
|
|
28
28
|
CACHE_CONTROL = { type: "ephemeral" }.freeze
|
|
29
29
|
|
|
30
|
+
# Left unset, the provider reserves the model's advertised maximum output
|
|
31
|
+
# ahead of the prompt (qwen3.8-27b: 128K of a 256K window), halving the
|
|
32
|
+
# history an agent turn of a few hundred tokens can build on.
|
|
33
|
+
MAX_OUTPUT_TOKENS = 32_768
|
|
34
|
+
|
|
30
35
|
EXEC_ENV = {
|
|
31
36
|
"PAGER" => "cat",
|
|
32
37
|
"MANPAGER" => "cat",
|
|
@@ -224,7 +229,7 @@ module Miniswen
|
|
|
224
229
|
end
|
|
225
230
|
|
|
226
231
|
CostSource = Data.define(:name, :model, :priced_as, :registry) do
|
|
227
|
-
def to_h = { name
|
|
232
|
+
def to_h = { name:, model:, priced_as:, registry: }.compact
|
|
228
233
|
end
|
|
229
234
|
|
|
230
235
|
attr_reader :messages, :environment
|
|
@@ -503,7 +508,7 @@ module Miniswen
|
|
|
503
508
|
tools: { bash: @bash_tool },
|
|
504
509
|
temperature: nil,
|
|
505
510
|
model: model_info,
|
|
506
|
-
params: routing_params,
|
|
511
|
+
params: routing_params.merge(output_cap_params(model_info)),
|
|
507
512
|
thinking: (RubyLLM::Thinking::Config.new(effort: @effort) if @effort)
|
|
508
513
|
)
|
|
509
514
|
payload(response)
|
|
@@ -543,6 +548,20 @@ module Miniswen
|
|
|
543
548
|
|
|
544
549
|
def provider_order = ENV["LEMANS_PROVIDER_ORDER"] || ENV["OPENROUTER_PROVIDER_ORDER"]
|
|
545
550
|
|
|
551
|
+
# OpenAI itself retired `max_tokens` for its reasoning models; the
|
|
552
|
+
# OpenAI-compatible providers and Anthropic still read it.
|
|
553
|
+
def output_cap_params(model_info)
|
|
554
|
+
cap = [ info&.max_tokens, MAX_OUTPUT_TOKENS ].compact.min
|
|
555
|
+
provider_class = RubyLLM::Provider.providers[model_info.provider.to_sym]
|
|
556
|
+
if [ RubyLLM::Providers::OpenAI, RubyLLM::Providers::Azure ].include?(provider_class)
|
|
557
|
+
{ max_completion_tokens: cap }
|
|
558
|
+
elsif provider_class <= RubyLLM::Providers::OpenAI || provider_class <= RubyLLM::Providers::Anthropic
|
|
559
|
+
{ max_tokens: cap }
|
|
560
|
+
else
|
|
561
|
+
{}
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
|
|
546
565
|
def cost_source
|
|
547
566
|
if local?
|
|
548
567
|
return CostSource.new(name: :local_provider, model: @model,
|
data/lib/miniswen/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lemans
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Svyatoslav Kryukov
|
|
@@ -163,6 +163,7 @@ files:
|
|
|
163
163
|
- lib/lemans/cli.rb
|
|
164
164
|
- lib/lemans/cli/board_reporter.rb
|
|
165
165
|
- lib/lemans/cli/progress_reporter.rb
|
|
166
|
+
- lib/lemans/cli/regrade.rb
|
|
166
167
|
- lib/lemans/cli/report.rb
|
|
167
168
|
- lib/lemans/cli/report/aggregate.rb
|
|
168
169
|
- lib/lemans/cli/templates/bench/README.md
|
|
@@ -170,7 +171,9 @@ files:
|
|
|
170
171
|
- lib/lemans/cli/templates/bench/environment/Dockerfile
|
|
171
172
|
- lib/lemans/cli/templates/bench/tasks/example-task/environment.patch
|
|
172
173
|
- lib/lemans/cli/templates/bench/tasks/example-task/instruction.md
|
|
173
|
-
- lib/lemans/cli/templates/bench/tasks/example-task/solution.patch
|
|
174
|
+
- lib/lemans/cli/templates/bench/tasks/example-task/solution.1.patch
|
|
175
|
+
- lib/lemans/cli/templates/bench/tasks/example-task/solution.2.patch
|
|
176
|
+
- lib/lemans/cli/templates/bench/tasks/example-task/verification_test.1.rb
|
|
174
177
|
- lib/lemans/cli/templates/bench/tasks/example-task/verification_test.rb
|
|
175
178
|
- lib/lemans/cli/templates/bench/tasks/hello-world/environment.patch
|
|
176
179
|
- lib/lemans/cli/templates/bench/tasks/hello-world/instruction.md
|
|
@@ -196,6 +199,7 @@ files:
|
|
|
196
199
|
- lib/lemans/environments/daytona/shell.rb
|
|
197
200
|
- lib/lemans/environments/daytona/snapshot_store.rb
|
|
198
201
|
- lib/lemans/environments/docker.rb
|
|
202
|
+
- lib/lemans/ext/deep_merge.rb
|
|
199
203
|
- lib/lemans/result.rb
|
|
200
204
|
- lib/lemans/runner.rb
|
|
201
205
|
- lib/lemans/runner/executor.rb
|