lemans 1.0.0.rc.1 → 1.0.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -1
  3. data/README.md +57 -37
  4. data/exe/lemans-remote +42 -40
  5. data/lib/lemans/agents/miniswen_installed.rb +4 -4
  6. data/lib/lemans/cli/board_reporter.rb +8 -8
  7. data/lib/lemans/cli/progress_reporter.rb +1 -1
  8. data/lib/lemans/cli/report/aggregate.rb +2 -2
  9. data/lib/lemans/cli/report.rb +6 -6
  10. data/lib/lemans/cli/templates/bench/README.md +41 -0
  11. data/lib/lemans/cli/templates/bench/bench.yml +110 -0
  12. data/lib/lemans/cli/templates/bench/environment/Dockerfile +10 -0
  13. data/lib/lemans/cli/templates/bench/tasks/example-task/environment.patch +7 -0
  14. data/lib/lemans/cli/templates/bench/tasks/example-task/instruction.md +32 -0
  15. data/lib/lemans/cli/templates/bench/tasks/example-task/solution.patch +7 -0
  16. data/lib/lemans/cli/templates/bench/tasks/example-task/verification_test.rb +34 -0
  17. data/lib/lemans/cli/templates/bench/tasks/hello-world/environment.patch +7 -0
  18. data/lib/lemans/cli/templates/bench/tasks/hello-world/instruction.md +11 -0
  19. data/lib/lemans/cli/templates/bench/tasks/hello-world/solution.patch +7 -0
  20. data/lib/lemans/cli/templates/bench/tasks/hello-world/verification_test.rb +10 -0
  21. data/lib/lemans/cli.rb +14 -3
  22. data/lib/lemans/config/environment.rb +8 -1
  23. data/lib/lemans/config/revision.rb +2 -0
  24. data/lib/lemans/config/setup.rb +1 -15
  25. data/lib/lemans/config/tree_digest.rb +1 -1
  26. data/lib/lemans/config/verifier.rb +1 -1
  27. data/lib/lemans/config.rb +10 -2
  28. data/lib/lemans/environments/daytona/faraday_transfer.rb +1 -1
  29. data/lib/lemans/environments/daytona/retries.rb +1 -1
  30. data/lib/lemans/environments/daytona/sdk_tweaks.rb +2 -2
  31. data/lib/lemans/environments/daytona/shell.rb +1 -1
  32. data/lib/lemans/environments/daytona/snapshot_store.rb +2 -2
  33. data/lib/lemans/environments/daytona.rb +1 -1
  34. data/lib/lemans/environments/docker.rb +170 -0
  35. data/lib/lemans/environments.rb +1 -1
  36. data/lib/lemans/result.rb +3 -1
  37. data/lib/lemans/runner/task.rb +1 -1
  38. data/lib/lemans/task_definition.rb +10 -8
  39. data/lib/lemans/trial/verifier/assets/lemans_minitest_reporter.rb +2 -2
  40. data/lib/lemans/trial/verifier.rb +3 -3
  41. data/lib/lemans/trial.rb +2 -2
  42. data/lib/lemans/version.rb +1 -1
  43. data/lib/lemans.rb +2 -0
  44. data/lib/miniswen/agent.rb +11 -11
  45. data/lib/miniswen/ruby_llm.rb +7 -1
  46. data/lib/miniswen/testing.rb +7 -7
  47. data/lib/miniswen/trajectory.rb +1 -1
  48. data/lib/miniswen/version.rb +1 -1
  49. metadata +15 -1
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "securerandom"
5
+
6
+ module Lemans
7
+ module Environments
8
+ # Local containers driven through the docker CLI
9
+ class Docker < Environment
10
+ DEFAULT_BUILD_TIMEOUT = 600
11
+
12
+ HOUSEKEEPING_TIMEOUT = 60
13
+ MAX_OUTPUT_BYTES = 200_000
14
+ EXEC_SLACK = 30
15
+
16
+ attr_reader :container
17
+
18
+ def initialize(image:, resources:, network:, env: {}, labels: {}, logger: nil, build_timeout: nil)
19
+ super(image:, resources:, network:, env:, labels:,
20
+ build_timeout: build_timeout || DEFAULT_BUILD_TIMEOUT)
21
+ @logger = logger
22
+ @name = "lemans-#{SecureRandom.hex(6)}"
23
+ assert_policy_supported!(network)
24
+ end
25
+
26
+ def start
27
+ build_image! if image.built?
28
+ docker!("run", *run_args, timeout: build_timeout)
29
+ @container = @name
30
+ self
31
+ rescue InfrastructureError => e
32
+ remove
33
+ raise InfrastructureError, "docker: could not start container: #{e.message}"
34
+ end
35
+
36
+ def exec(command, timeout: nil, env: {})
37
+ timeout ||= DEFAULT_TIMEOUT
38
+ started = now
39
+ argv = [ "exec" ]
40
+ env.each { |key, value| argv += [ "--env", "#{key}=#{value}" ] }
41
+ # The in-container timeout is what actually kills the process
42
+ argv += [ container, "timeout", timeout.to_i.to_s, "sh", "-c", command ]
43
+
44
+ exit_code, output = capture("docker", *argv, timeout: timeout + EXEC_SLACK)
45
+ ExecResult.new(command:, exit_code:, output:, duration: (now - started).round(3))
46
+ end
47
+
48
+ def upload(local_path, remote_path)
49
+ docker!("exec", container, "mkdir", "-p", File.dirname(remote_path.to_s))
50
+ docker!("cp", local_path.to_s, "#{container}:#{remote_path}")
51
+ end
52
+
53
+ def download(remote_path, local_path)
54
+ local_path = Pathname(local_path)
55
+ local_path.dirname.mkpath
56
+ docker!("cp", "#{container}:#{remote_path}", local_path.to_s)
57
+ end
58
+
59
+ def switch_network_policy!(policy)
60
+ assert_policy_supported!(policy)
61
+
62
+ if policy.none?
63
+ connected_networks.each { docker!("network", "disconnect", it, container) }
64
+ else
65
+ networks = connected_networks
66
+ docker!("network", "disconnect", "none", container) if networks.include?("none")
67
+ docker!("network", "connect", "bridge", container) unless networks.include?("bridge")
68
+ end
69
+
70
+ @network = policy
71
+ end
72
+
73
+ def stop
74
+ return if container.nil?
75
+
76
+ exit_code, output = capture("docker", "rm", "--force", "--volumes", container, timeout: HOUSEKEEPING_TIMEOUT)
77
+ if exit_code.zero?
78
+ @container = nil
79
+ else
80
+ warn "lemans: container #{container} may still be running — remove failed: #{output.strip}"
81
+ end
82
+ rescue StandardError => e
83
+ warn "lemans: container #{container} may still be running — remove failed: #{e.class}: #{e.message}"
84
+ end
85
+
86
+ private
87
+
88
+ def assert_policy_supported!(policy)
89
+ return if policy.none? || policy.public?
90
+
91
+ raise ConfigError, "docker: #{policy.mode} is not supported (public and none only)"
92
+ end
93
+
94
+ # The tag is the content digest, so an existing image is the identical thing
95
+ def build_image!
96
+ exists, = capture("docker", "image", "inspect", image.name, timeout: HOUSEKEEPING_TIMEOUT)
97
+ return if exists.zero?
98
+
99
+ docker!("build", "--tag", image.name, image.context_dir.to_s, timeout: build_timeout, on_output: @logger)
100
+ end
101
+
102
+ def run_args
103
+ args = [ "--detach", "--init", "--name", @name,
104
+ "--cpus", resources.cpus.to_s, "--memory", "#{resources.memory}m",
105
+ "--entrypoint", "sh" ]
106
+ args += [ "--network", "none" ] if network.none?
107
+ env.each { |key, value| args += [ "--env", "#{key}=#{value}" ] }
108
+ labels.each { |key, value| args += [ "--label", "#{key}=#{value}" ] }
109
+ args + [ image.name, "-c", "tail -f /dev/null" ]
110
+ end
111
+
112
+ def connected_networks
113
+ docker!("inspect", "--format", "{{range $name, $_ := .NetworkSettings.Networks}}{{$name}} {{end}}", container).split
114
+ end
115
+
116
+ def remove
117
+ capture("docker", "rm", "--force", "--volumes", @name, timeout: HOUSEKEEPING_TIMEOUT)
118
+ rescue StandardError
119
+ nil
120
+ end
121
+
122
+ def docker!(*argv, timeout: HOUSEKEEPING_TIMEOUT, on_output: nil)
123
+ exit_code, output = capture("docker", *argv, timeout:, on_output:)
124
+ return output if exit_code.zero?
125
+
126
+ raise InfrastructureError, "docker #{argv.first(2).join(" ")} exited #{exit_code}: #{output[0, 2000]}"
127
+ end
128
+
129
+ def capture(*argv, timeout:, on_output: nil)
130
+ Open3.popen2e(*argv) do |stdin, pipe, wait|
131
+ stdin.close
132
+ deadline = now + timeout
133
+ output = +""
134
+ timed_out = false
135
+
136
+ loop do
137
+ remaining = deadline - now
138
+ if remaining <= 0
139
+ timed_out = true
140
+ kill(wait.pid)
141
+ break
142
+ end
143
+ next unless pipe.wait_readable(remaining)
144
+
145
+ chunk = pipe.read_nonblock(65_536, exception: false)
146
+ break if chunk.nil?
147
+ next if chunk == :wait_readable
148
+
149
+ on_output&.call(chunk)
150
+ output << chunk
151
+ output = output.byteslice(-MAX_OUTPUT_BYTES..) if output.bytesize > MAX_OUTPUT_BYTES
152
+ end
153
+
154
+ status = wait.value
155
+ [ timed_out ? 124 : (status.exitstatus || 1), output.scrub ]
156
+ end
157
+ rescue Errno::ENOENT
158
+ raise ConfigError, "docker: CLI not found — install Docker or pick another backend"
159
+ end
160
+
161
+ def kill(pid)
162
+ Process.kill("KILL", pid)
163
+ rescue Errno::ESRCH
164
+ nil
165
+ end
166
+
167
+ def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
168
+ end
169
+ end
170
+ end
@@ -4,7 +4,7 @@ module Lemans
4
4
  # The backends `lemans run --backend` can name, and the one place a name
5
5
  # becomes a class. An unknown backend fails as a config mistake.
6
6
  module Environments
7
- BACKENDS = { "daytona" => "Daytona" }.freeze
7
+ BACKENDS = { "daytona" => "Daytona", "docker" => "Docker" }.freeze
8
8
 
9
9
  def self.build(backend, **)
10
10
  constant = BACKENDS[backend.to_s] or
data/lib/lemans/result.rb CHANGED
@@ -68,7 +68,7 @@ module Lemans
68
68
  def Usage.from_json(data)
69
69
  # Older files carry a partial cost_source (just the name).
70
70
  if (source = data[:cost_source])
71
- cost_source = CostSource.new(**CostSource.members.to_h { [it, nil] }, **source)
71
+ cost_source = CostSource.new(**CostSource.members.to_h { [ it, nil ] }, **source)
72
72
  end
73
73
  new(
74
74
  input_tokens: data[:input_tokens],
@@ -167,6 +167,8 @@ module Lemans
167
167
 
168
168
  def detail = outcome.detail
169
169
 
170
+ def pending? = outcome.pending?
171
+
170
172
  def scored? = outcome.scored?
171
173
 
172
174
  def invalid? = outcome.invalid?
@@ -49,7 +49,7 @@ module Lemans
49
49
  reporter&.record(:finished, result)
50
50
  result
51
51
  ensure
52
- store&.save(result)
52
+ store&.save(result) unless result.pending?
53
53
  end
54
54
 
55
55
  private
@@ -75,12 +75,12 @@ module Lemans
75
75
  "what has to vary belongs in bench.yml, where it varies for every trial"
76
76
  end
77
77
 
78
- if (extras = data["verifier"].is_a?(Hash) ? data["verifier"].keys - ["setup"] : nil) && extras.any?
78
+ if (extras = data["verifier"].is_a?(Hash) ? data["verifier"].keys - [ "setup" ] : nil) && extras.any?
79
79
  raise ConfigError, "#{task.dir}: a task may only override verifier.setup, not verifier.#{extras.first}"
80
80
  end
81
81
 
82
- raise ConfigError, "#{task.dir}: #{ENVIRONMENT_DIR}/Dockerfile is required when bench.yml names no shared image" unless
83
- task.config.environment.image || task.environment_dockerfile.file?
82
+ raise ConfigError, "#{task.dir}: #{ENVIRONMENT_DIR}/Dockerfile is required when the bench declares no shared image or dockerfile" unless
83
+ task.config.environment.image || task.config.environment.dockerfile || task.environment_dockerfile.file?
84
84
 
85
85
  return unless task.test_files.empty?
86
86
 
@@ -164,10 +164,12 @@ module Lemans
164
164
  def environment_dockerfile = dir.join(ENVIRONMENT_DIR, "Dockerfile")
165
165
 
166
166
  def environment_image
167
- if environment.image
167
+ if environment_dockerfile.file?
168
+ Config::ImageSpec.dockerfile(environment_dockerfile, slug: name)
169
+ elsif environment.image
168
170
  Config::ImageSpec.registry(environment.image)
169
171
  else
170
- Config::ImageSpec.dockerfile(environment_dockerfile, slug: name)
172
+ Config::ImageSpec.dockerfile(environment.dockerfile, slug: "shared")
171
173
  end
172
174
  end
173
175
 
@@ -177,18 +179,18 @@ module Lemans
177
179
  declared = @declared_setup || Config::Setup.new
178
180
  return declared unless seed? && declared.files.none? { |_, remote| remote == FLAT_SEED }
179
181
 
180
- declared.merge(Config::Setup.new.tap { it.files = [[dir.join(FLAT_SEED), FLAT_SEED]] })
182
+ declared.merge(Config::Setup.new.tap { it.files = [ [ dir.join(FLAT_SEED), FLAT_SEED ] ] })
181
183
  end
182
184
 
183
185
  # Dotfiles included, matching TreeDigest: the files a digest records are
184
186
  # exactly the files that ship.
185
187
  def expand(root)
186
- root.glob("**/*", File::FNM_DOTMATCH).select(&:file?).map { [it, it.relative_path_from(root).to_s] }
188
+ root.glob("**/*", File::FNM_DOTMATCH).select(&:file?).map { [ it, it.relative_path_from(root).to_s ] }
187
189
  end
188
190
 
189
191
  def flat(filename)
190
192
  path = dir.join(filename)
191
- path.file? ? [[path, filename]] : []
193
+ path.file? ? [ [ path, filename ] ] : []
192
194
  end
193
195
  end
194
196
  end
@@ -17,11 +17,11 @@ module LemansReport
17
17
  end
18
18
 
19
19
  def report
20
- graded = @results.select { graded?(_1) }
20
+ graded = @results.select { graded?(it) }
21
21
  prior = existing.fetch("checks", {})
22
22
  return if graded.empty? && prior.empty?
23
23
 
24
- checks = prior.merge(graded.to_h { [name(_1), status(_1)] }).sort.to_h
24
+ checks = prior.merge(graded.to_h { [ name(it), status(it) ] }).sort.to_h
25
25
  File.write(
26
26
  File.join(@dir, "checks.json"),
27
27
  JSON.pretty_generate(checks: checks, failures: checks.reject { |_, status| status == "pass" }.keys)
@@ -64,8 +64,8 @@ module Lemans
64
64
  def upload_tests!
65
65
  environment.exec!("rm -rf #{TESTS_DIR} && mkdir -p #{TESTS_DIR}")
66
66
 
67
- uploads = task.verifier.files.to_h { |local, remote| [remote, local] }
68
- .merge(task.test_files.to_h { |local, remote| [remote, local] })
67
+ uploads = task.verifier.files.to_h { |local, remote| [ remote, local ] }
68
+ .merge(task.test_files.to_h { |local, remote| [ remote, local ] })
69
69
 
70
70
  uploads.each { |remote, local| environment.upload(local, "#{TESTS_DIR}/#{remote}") }
71
71
  ASSETS.glob("*.rb").each { |asset| environment.upload(asset, "#{TESTS_DIR}/#{asset.basename}") }
@@ -102,7 +102,7 @@ module Lemans
102
102
  end
103
103
 
104
104
  def verifier_script
105
- [task.verifier.preverify, task.verifier.command].compact.map { "( #{it} )" }.join(" && ")
105
+ [ task.verifier.preverify, task.verifier.command ].compact.map { "( #{it} )" }.join(" && ")
106
106
  end
107
107
 
108
108
  def read_reward(command_result)
data/lib/lemans/trial.rb CHANGED
@@ -36,7 +36,7 @@ module Lemans
36
36
  build_timeout: task.environment.build_timeout,
37
37
  labels: {
38
38
  "lemans.task" => task.name,
39
- "lemans.trial" => result.id,
39
+ "lemans.trial" => self.result.id,
40
40
  "lemans.phase" => "agent"
41
41
  }
42
42
  )
@@ -118,7 +118,7 @@ module Lemans
118
118
  raise
119
119
  rescue StandardError => e
120
120
  # A harness bug must leave evidence.
121
- result.failed!(:harness_crash, ["#{e.class}: #{e.message}", *Array(e.backtrace).first(5)].join("\n"))
121
+ result.failed!(:harness_crash, [ "#{e.class}: #{e.message}", *Array(e.backtrace).first(5) ].join("\n"))
122
122
  ensure
123
123
  environment&.stop
124
124
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lemans
4
- VERSION = "1.0.0.rc.1"
4
+ VERSION = "1.0.0"
5
5
  end
data/lib/lemans.rb CHANGED
@@ -9,6 +9,8 @@ loader.inflector.inflect("cli" => "CLI", "atif" => "ATIF", "fs" => "FS",
9
9
  loader.ignore("#{__dir__}/miniswen.rb", "#{__dir__}/miniswen")
10
10
  # assets/ holds files uploaded into sandboxes, not Ruby the harness loads.
11
11
  loader.ignore("#{__dir__}/lemans/trial/verifier/assets")
12
+ # templates/ holds the files `lemans init` scaffolds, not Ruby the harness loads.
13
+ loader.ignore("#{__dir__}/lemans/cli/templates")
12
14
  loader.setup
13
15
 
14
16
  require "miniswen"
@@ -215,9 +215,9 @@ module Miniswen
215
215
  value.to_h do |key, item|
216
216
  key = key.to_sym
217
217
  # Tool-call arguments and reasoning details keep their provider-style string keys.
218
- [key, %i[arguments reasoning_details].include?(key) ? item : deep_symbolize(item)]
218
+ [ key, %i[arguments reasoning_details].include?(key) ? item : deep_symbolize(item) ]
219
219
  end
220
- when Array then value.map { deep_symbolize(_1) }
220
+ when Array then value.map { deep_symbolize(it) }
221
221
  else value
222
222
  end
223
223
  end
@@ -313,7 +313,7 @@ module Miniswen
313
313
  # them back from ENV on boot (the option upcased).
314
314
  def provider_env
315
315
  _, provider = resolved
316
- env = provider.configuration_requirements.to_h { [_1.to_s.upcase, RubyLLM.config.public_send(_1)] }.compact
316
+ env = provider.configuration_requirements.to_h { [ it.to_s.upcase, RubyLLM.config.public_send(it) ] }.compact
317
317
 
318
318
  order = ENV["LEMANS_PROVIDER_ORDER"]
319
319
  env["LEMANS_PROVIDER_ORDER"] = order if order
@@ -340,7 +340,7 @@ module Miniswen
340
340
  def next_actions
341
341
  response = complete(@messages)
342
342
  @steps += 1
343
- @totals.each_key { @totals[_1] += response[_1].to_i }
343
+ @totals.each_key { @totals[it] += response[it].to_i }
344
344
  track_cost(response)
345
345
 
346
346
  entry = { role: "assistant", timestamp: Time.now.utc.iso8601, content: response[:content].to_s,
@@ -496,7 +496,7 @@ module Miniswen
496
496
  def complete(messages)
497
497
  model_info, provider = resolved
498
498
  response = provider.complete(
499
- with_cache_breakpoints(messages.map { as_ruby_llm(_1) }),
499
+ with_cache_breakpoints(messages.map { as_ruby_llm(it) }),
500
500
  tools: { bash: @bash_tool },
501
501
  temperature: nil,
502
502
  model: model_info,
@@ -518,13 +518,13 @@ module Miniswen
518
518
  def with_cache_breakpoints(messages)
519
519
  return messages unless explicit_cache?
520
520
 
521
- system = messages.find { _1.role == :system }
522
- [system, messages.last].compact.uniq.each do |message|
521
+ system = messages.find { it.role == :system }
522
+ [ system, messages.last ].compact.uniq.each do |message|
523
523
  text = message.content
524
524
  next unless text.is_a?(String) && !text.empty?
525
525
 
526
526
  message.content = RubyLLM::Content::Raw.new(
527
- [{ type: "text", text: text, cache_control: CACHE_CONTROL }]
527
+ [ { type: "text", text: text, cache_control: CACHE_CONTROL } ]
528
528
  )
529
529
  end
530
530
  messages
@@ -580,8 +580,8 @@ module Miniswen
580
580
  return nil if tool_calls.nil? || tool_calls.empty?
581
581
 
582
582
  tool_calls.to_h do |call|
583
- [call[:id], RubyLLM::ToolCall.new(id: call[:id], name: call[:name], arguments: call[:arguments],
584
- thought_signature: call[:thought_signature])]
583
+ [ call[:id], RubyLLM::ToolCall.new(id: call[:id], name: call[:name], arguments: call[:arguments],
584
+ thought_signature: call[:thought_signature]) ]
585
585
  end
586
586
  end
587
587
 
@@ -668,7 +668,7 @@ module Miniswen
668
668
  cache_write = info.cache_write_input_price_per_million || input
669
669
  # Providers usually fold thinking into output_tokens; max() bills the
670
670
  # larger count once and can never double-bill.
671
- generated = [response.output_tokens.to_i, tokens&.thinking.to_i].max
671
+ generated = [ response.output_tokens.to_i, tokens&.thinking.to_i ].max
672
672
  ((response.input_tokens.to_i * input) +
673
673
  (tokens&.cached.to_i * cache_read) +
674
674
  (tokens&.cache_creation.to_i * cache_write) +
@@ -8,6 +8,12 @@ RubyLLM.configure do |config|
8
8
  config.logger = Logger.new(IO::NULL) unless ENV["MINISWEN_DEBUG"] == "1"
9
9
  end
10
10
 
11
+ # Increase retry window to handle egress network issues
12
+ RubyLLM.configure do |config|
13
+ config.max_retries = 3
14
+ config.retry_interval = 1
15
+ end
16
+
11
17
  # ruby_llm reads no API keys from ENV on its own; the conventional variable is the provider's
12
18
  # config option upcased.
13
19
  RubyLLM.configure do |config|
@@ -34,7 +40,7 @@ module Miniswen
34
40
  # ConnectionFailed retries ruby_llm already does; it only lists SSL errors
35
41
  # as fatal.
36
42
  module RetryTransientSSL
37
- def retry_exceptions = super + [Faraday::SSLError]
43
+ def retry_exceptions = super + [ Faraday::SSLError ]
38
44
  end
39
45
  end
40
46
 
@@ -60,14 +60,14 @@ module Miniswen
60
60
  end
61
61
 
62
62
  def on(command, output, exit_code: 0)
63
- @canned[command] = [exit_code, output]
63
+ @canned[command] = [ exit_code, output ]
64
64
  end
65
65
 
66
66
  def exec(command, timeout: nil, env: nil) # rubocop:disable Lint/UnusedMethodArgument
67
67
  @commands << command
68
68
  return result(0, command.delete_prefix("echo ")) if command.start_with?("echo ")
69
69
 
70
- exit_code, output = @canned.fetch(command, [0, ""])
70
+ exit_code, output = @canned.fetch(command, [ 0, "" ])
71
71
  result(exit_code, output)
72
72
  end
73
73
 
@@ -87,7 +87,7 @@ module Miniswen
87
87
  # and/or :content, plus optional overrides (:tool_calls, :finish_reason,
88
88
  # :thinking, token counts). Overrides given here apply to every answer.
89
89
  def stub_llm(*answers, **overrides)
90
- RubyLLM::Test.stub_responses(*answers.map { llm_answer(_1, **overrides) })
90
+ RubyLLM::Test.stub_responses(*answers.map { llm_answer(it, **overrides) })
91
91
  end
92
92
 
93
93
  def llm_answer(answer, **overrides)
@@ -115,15 +115,15 @@ module Miniswen
115
115
  def raw_body_for(answer, calls)
116
116
  choice = { "finish_reason" => answer.fetch(:finish_reason) { calls.empty? ? "stop" : "tool_calls" } }
117
117
  choice["message"] = { "reasoning_details" => answer[:reasoning_details] } if answer[:reasoning_details]
118
- { "choices" => [choice] }
118
+ { "choices" => [ choice ] }
119
119
  end
120
120
 
121
121
  def tool_calls_for(answer)
122
- calls = answer[:tool_calls] || Array(answer[:cmd]).map { { name: "bash", arguments: { "command" => _1 } } }
122
+ calls = answer[:tool_calls] || Array(answer[:cmd]).map { { name: "bash", arguments: { "command" => it } } }
123
123
  calls.to_h do |call|
124
124
  id = call[:id] || "call_#{@llm_answer_ids = @llm_answer_ids.to_i + 1}"
125
- [id, RubyLLM::ToolCall.new(id: id, name: call[:name], arguments: call[:arguments],
126
- thought_signature: call[:thought_signature])]
125
+ [ id, RubyLLM::ToolCall.new(id: id, name: call[:name], arguments: call[:arguments],
126
+ thought_signature: call[:thought_signature]) ]
127
127
  end
128
128
  end
129
129
  end
@@ -83,7 +83,7 @@ module Miniswen
83
83
  end
84
84
 
85
85
  def tool_calls_for(calls)
86
- calls.map { { tool_call_id: _1[:id], function_name: _1[:name], arguments: _1[:arguments] } }
86
+ calls.map { { tool_call_id: it[:id], function_name: it[:name], arguments: it[:arguments] } }
87
87
  end
88
88
 
89
89
  def metrics_for(metrics)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Miniswen
4
- VERSION = "0.1.1"
4
+ VERSION = "1.0.0"
5
5
  end
metadata CHANGED
@@ -1,11 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lemans
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.rc.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Svyatoslav Kryukov
8
8
  - Artur Petrov
9
+ - Vladimir Dementyev
9
10
  bindir: exe
10
11
  cert_chain: []
11
12
  date: 1980-01-02 00:00:00.000000000 Z
@@ -140,6 +141,7 @@ description: Lemans runs tasks against a coding agent in a sandbox.
140
141
  email:
141
142
  - me@skryukov.dev
142
143
  - ardecvz@gmail.com
144
+ - dementiev.vm@gmail.com
143
145
  executables:
144
146
  - lemans
145
147
  - lemans-remote
@@ -163,6 +165,17 @@ files:
163
165
  - lib/lemans/cli/progress_reporter.rb
164
166
  - lib/lemans/cli/report.rb
165
167
  - lib/lemans/cli/report/aggregate.rb
168
+ - lib/lemans/cli/templates/bench/README.md
169
+ - lib/lemans/cli/templates/bench/bench.yml
170
+ - lib/lemans/cli/templates/bench/environment/Dockerfile
171
+ - lib/lemans/cli/templates/bench/tasks/example-task/environment.patch
172
+ - 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/verification_test.rb
175
+ - lib/lemans/cli/templates/bench/tasks/hello-world/environment.patch
176
+ - lib/lemans/cli/templates/bench/tasks/hello-world/instruction.md
177
+ - lib/lemans/cli/templates/bench/tasks/hello-world/solution.patch
178
+ - lib/lemans/cli/templates/bench/tasks/hello-world/verification_test.rb
166
179
  - lib/lemans/clobber.rb
167
180
  - lib/lemans/config.rb
168
181
  - lib/lemans/config/agent.rb
@@ -182,6 +195,7 @@ files:
182
195
  - lib/lemans/environments/daytona/sdk_tweaks.rb
183
196
  - lib/lemans/environments/daytona/shell.rb
184
197
  - lib/lemans/environments/daytona/snapshot_store.rb
198
+ - lib/lemans/environments/docker.rb
185
199
  - lib/lemans/result.rb
186
200
  - lib/lemans/runner.rb
187
201
  - lib/lemans/runner/executor.rb