lemans 0.2.2 → 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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +30 -10
  3. data/exe/lemans-remote +161 -46
  4. data/lib/lemans/agent.rb +39 -0
  5. data/lib/lemans/agents/miniswen.rb +28 -25
  6. data/lib/lemans/agents/miniswen_installed.rb +13 -9
  7. data/lib/lemans/agents/nop.rb +3 -3
  8. data/lib/lemans/agents/oracle.rb +8 -8
  9. data/lib/lemans/cli/board_reporter.rb +16 -16
  10. data/lib/lemans/cli/progress_reporter.rb +19 -19
  11. data/lib/lemans/cli/report/aggregate.rb +117 -0
  12. data/lib/lemans/cli/report.rb +164 -0
  13. data/lib/lemans/cli.rb +58 -63
  14. data/lib/lemans/clobber.rb +17 -55
  15. data/lib/lemans/config/agent.rb +51 -0
  16. data/lib/lemans/config/conversion.rb +62 -0
  17. data/lib/lemans/config/environment.rb +39 -0
  18. data/lib/lemans/config/image_spec.rb +42 -0
  19. data/lib/lemans/config/network_policy.rb +55 -0
  20. data/lib/lemans/config/revision.rb +42 -0
  21. data/lib/lemans/config/setup.rb +57 -0
  22. data/lib/lemans/config/tree_digest.rb +26 -0
  23. data/lib/lemans/config/verifier.rb +72 -0
  24. data/lib/lemans/config.rb +107 -0
  25. data/lib/lemans/environment.rb +54 -0
  26. data/lib/lemans/environments/daytona/faraday_transfer.rb +172 -0
  27. data/lib/lemans/environments/daytona/sdk_tweaks.rb +1 -1
  28. data/lib/lemans/environments/daytona/shell.rb +1 -1
  29. data/lib/lemans/environments/daytona/snapshot_store.rb +11 -11
  30. data/lib/lemans/environments/daytona.rb +21 -35
  31. data/lib/lemans/result.rb +270 -0
  32. data/lib/lemans/runner/executor.rb +64 -0
  33. data/lib/lemans/runner/task.rb +62 -0
  34. data/lib/lemans/runner.rb +82 -0
  35. data/lib/lemans/store.rb +44 -0
  36. data/lib/lemans/stores/fs.rb +122 -0
  37. data/lib/lemans/task_definition.rb +194 -0
  38. data/lib/lemans/trial/patch.rb +76 -0
  39. data/lib/lemans/trial/setup.rb +66 -0
  40. data/lib/lemans/trial/snapshot.rb +57 -0
  41. data/lib/lemans/trial/verifier.rb +190 -0
  42. data/lib/lemans/trial.rb +113 -147
  43. data/lib/lemans/version.rb +1 -1
  44. data/lib/lemans.rb +3 -2
  45. data/lib/miniswen/trajectory.rb +2 -0
  46. metadata +58 -25
  47. data/lib/lemans/agents/base.rb +0 -30
  48. data/lib/lemans/bench.rb +0 -280
  49. data/lib/lemans/environments/base.rb +0 -55
  50. data/lib/lemans/network_policy.rb +0 -66
  51. data/lib/lemans/patch.rb +0 -70
  52. data/lib/lemans/restore_paths.rb +0 -21
  53. data/lib/lemans/results/aggregate.rb +0 -114
  54. data/lib/lemans/results/cost_source.rb +0 -13
  55. data/lib/lemans/results/outcome.rb +0 -36
  56. data/lib/lemans/results/report.rb +0 -149
  57. data/lib/lemans/results/sorting.rb +0 -24
  58. data/lib/lemans/results/tally.rb +0 -19
  59. data/lib/lemans/results/usage.rb +0 -24
  60. data/lib/lemans/run.rb +0 -152
  61. data/lib/lemans/setup.rb +0 -59
  62. data/lib/lemans/setup_files.rb +0 -36
  63. data/lib/lemans/snapshot.rb +0 -55
  64. data/lib/lemans/task.rb +0 -207
  65. data/lib/lemans/tree_digest.rb +0 -24
  66. data/lib/lemans/units.rb +0 -44
  67. data/lib/lemans/verifier.rb +0 -199
  68. /data/lib/lemans/{verifier → trial/verifier}/assets/eport-lemans.rb +0 -0
  69. /data/lib/lemans/{verifier → trial/verifier}/assets/lemans_minitest_reporter.rb +0 -0
data/lib/lemans/trial.rb CHANGED
@@ -1,187 +1,153 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
- require "securerandom"
4
+ require "pathname"
5
5
  require "time"
6
6
 
7
7
  module Lemans
8
8
  # One task, one agent, one reward. Only what happens inside the agent phase
9
9
  # is a statement about the model; everything else is the harness's fault.
10
+ # Runs standalone: `Trial.new(task).run` needs no runner machinery.
10
11
  class Trial
11
- attr_reader :task, :bench, :agent_name, :model, :backend, :dir, :id
12
+ attr_reader :task, :config, :model, :agent_name, :environment, :result
12
13
 
13
- def initialize(task:, bench:, agent_name:, runs_dir:, model: nil, backend: "daytona")
14
+ private attr_reader :agent, :store, :snapshot, :patch
15
+
16
+ def initialize(task, model = nil, result: nil, store: nil, agent: nil, environment: nil)
14
17
  @task = task
15
- @bench = bench
16
- @agent_name = agent_name
17
- @model = model
18
- @backend = backend
19
- @id = "#{task.name}__#{SecureRandom.alphanumeric(7)}"
20
- @dir = Pathname(runs_dir).join(model_dir, @id)
18
+ @config = task.config
19
+ @model = model || config.models.first
20
+ @store = store
21
+
22
+ @agent = agent.is_a?(Agent) ? agent : Agents.build(agent || config.agent_name, profile: config.agent, model: @model)
23
+ @agent_name = @agent.name
24
+
25
+ @result = result || Result.from_task(task, model: @model, agent: agent_name)
26
+
27
+ @environment =
28
+ if environment.is_a?(Environment)
29
+ environment
30
+ else
31
+ Environments.build(
32
+ environment || config.backend,
33
+ image: task.environment_image,
34
+ resources: task.environment.resources,
35
+ network: task.environment.network,
36
+ build_timeout: task.environment.build_timeout,
37
+ labels: {
38
+ "lemans.task" => task.name,
39
+ "lemans.trial" => result.id,
40
+ "lemans.phase" => "agent"
41
+ }
42
+ )
43
+ end
44
+
45
+ @snapshot = nil
46
+ @patch = nil
21
47
  end
22
48
 
23
49
  def run
24
- logs_dir.mkpath
25
- started_at = Time.now.utc
26
- reward = nil
27
- usage = nil
28
- outcome = Results::Outcome.new(:completed)
29
- @phases = {}
30
- # Declared outside the phase blocks they are assigned in, or `ensure`
31
- # could not stop a sandbox whose setup raised.
32
- environment = nil
33
- snapshot = nil
34
- patch = nil
35
-
36
- begin
37
- agent = Agents.build(agent_name, profile: bench.agent, model: model)
38
-
39
- phase(:environment_setup) do
40
- environment = start_environment
41
- prepare(environment)
42
-
43
- snapshot = Snapshot.new(environment, bench: bench, task: task,
44
- timeout: bench.environment.build_timeout_sec)
45
- snapshot.capture!
46
-
47
- patch = Patch.new(environment, bench: bench, dir: dir)
48
- patch.seal!
49
-
50
- agent.install(environment, task: task)
51
- environment.network_policy = bench.agent.network
52
- end
50
+ phase(:environment_setup) do
51
+ environment.start
53
52
 
54
- agent_result = phase(:agent) do
55
- in_agent_phase { agent.call(environment, task: task, logs_dir: logs_dir) }
56
- end
57
- usage = agent_result.usage
58
- outcome = over_ceiling(agent_result) || agent_result.outcome
53
+ # Run the setup commands and apply the seed patch
54
+ Setup.new(task, files: task.setup.files, commands: task.setup.commands, seed: task.seed?)
55
+ .execute!(environment)
59
56
 
60
- patch.collect!
57
+ # Capture the baseline state (used later for grading)
58
+ @snapshot = Snapshot.new(task, environment)
59
+ snapshot.capture!
61
60
 
62
- if outcome.scored?
63
- phase(:verifier) do
64
- # The sandbox is sealed before the tests arrive
65
- environment.network_policy = NetworkPolicy.none
66
- reward = Verifier.new(bench: bench, task: task, dir: dir, snapshot: snapshot).call(environment)
67
- end
61
+ # Seal the git state to collect the agent's patch later
62
+ @patch = Patch.new(task, environment)
63
+ patch.seal!
64
+
65
+ agent.install(task, environment)
66
+
67
+ environment.switch_network_policy!(config.agent.environment.network)
68
+ end
69
+
70
+ response =
71
+ phase(:agent) do
72
+ agent.run(task, environment)
73
+ rescue InfrastructureError, ::Miniswen::InfrastructureError => e
74
+ # Mark the failure here, where the agent phase is still known
75
+ result.failed!(:agent_error, e.message)
76
+ raise
68
77
  end
69
- rescue VerifierError => e
70
- outcome = Results::Outcome.new(:verifier_error, detail: e.message)
71
- rescue ::Miniswen::AccountingError => e
72
- outcome = Results::Outcome.new(:accounting_error, detail: e.message)
73
- rescue InfrastructureError, ::Miniswen::InfrastructureError => e
74
- outcome = Results::Outcome.new(@agent_phase ? :agent_error : :environment_error, detail: e.message)
75
- rescue ConfigError
76
- # A malformed bench is the author's bug to fix - raise!
77
- raise
78
- rescue StandardError => e
79
- # A harness bug must leave evidence.
80
- outcome = Results::Outcome.new(:harness_crash, detail: crash_detail(e))
81
- ensure
82
- environment&.stop
78
+
79
+ # Whatever the agent brought back is evidence, a failed run's included
80
+ save_trajectory(response.trajectory)
81
+ store&.save_artifact(result, response.raw_result, path: "agent.result.json") if response.raw_result
82
+
83
+ if response.error?
84
+ result.failed!(:agent_error, response.error)
85
+ return result
83
86
  end
84
87
 
85
- write_result(started_at: started_at, reward: reward, outcome: outcome, usage: usage)
86
- rescue SystemCallError, JSON::GeneratorError => e
87
- # runs_dir unwritable, disk full
88
- raise ConfigError, "cannot record trial #{id}: #{e.message}"
89
- end
88
+ result.completed!(response.outcome, response.usage)
90
89
 
91
- private
90
+ check_cost_limit!
92
91
 
93
- def model_dir
94
- (model || bench.agent.model || agent_name).to_s.split("/").last
95
- end
92
+ patch.collect!(result, store) if store
96
93
 
97
- def logs_dir = dir
94
+ if result.scored?
95
+ phase(:verifier) do
96
+ # The sandbox is sealed before the tests arrive
97
+ environment.switch_network_policy!(Config::NetworkPolicy.new("none"))
98
98
 
99
- def over_ceiling(result)
100
- limit = bench.agent.cost_limit
101
- cost = result.usage&.cost_usd
102
- return nil unless limit && cost && result.outcome.scored? && cost > limit
99
+ verification = Verifier.new(task, environment, snapshot).verify! do |evidence, path|
100
+ store&.save_artifact(result, evidence, path:)
101
+ end
103
102
 
104
- Results::Outcome.new(:cost_ceiling_reached,
105
- detail: format("spent $%<cost>.4f against a $%<limit>.4f limit", cost: cost, limit: limit))
106
- end
103
+ store&.save_artifact(result, verification.logs, path: "verifier.log")
107
104
 
108
- def crash_detail(error)
109
- ["#{error.class}: #{error.message}", *Array(error.backtrace).first(5)].join("\n")
110
- end
105
+ result.graded!(verification.reward)
106
+ end
107
+ end
111
108
 
112
- def phase(name)
113
- @phases[name] = { started_at: Time.now.utc.iso8601(6) }
114
- yield
109
+ result
110
+ rescue VerifierError => e
111
+ result.failed!(:verifier_error, e.message)
112
+ rescue ::Miniswen::AccountingError => e
113
+ result.failed!(:accounting_error, e.message)
114
+ rescue InfrastructureError, ::Miniswen::InfrastructureError => e
115
+ result.failed!(:environment_error, e.message)
116
+ rescue ConfigError
117
+ # A malformed bench is the author's bug to fix - raise!
118
+ raise
119
+ rescue StandardError => e
120
+ # A harness bug must leave evidence.
121
+ result.failed!(:harness_crash, ["#{e.class}: #{e.message}", *Array(e.backtrace).first(5)].join("\n"))
115
122
  ensure
116
- @phases[name][:finished_at] = Time.now.utc.iso8601(6)
123
+ environment&.stop
117
124
  end
118
125
 
119
- def in_agent_phase
120
- @agent_phase = true
121
- result = yield
122
- @agent_phase = false
123
- result
124
- end
126
+ private
125
127
 
126
- def start_environment
127
- Environments.build(
128
- backend,
129
- image: task.environment_image,
130
- resources: bench.environment.resources,
131
- network: bench.environment.network,
132
- build_timeout_sec: bench.environment.build_timeout_sec,
133
- labels: { "lemans.task" => task.name, "lemans.trial" => id, "lemans.phase" => "agent" }
134
- ).start
135
- end
128
+ def save_trajectory(trajectory)
129
+ return unless trajectory && store
136
130
 
137
- def prepare(environment)
138
- Setup.new(
139
- commands: bench.environment.setup,
140
- task: task,
141
- phase: :environment,
142
- timeout_sec: bench.environment.build_timeout_sec
143
- ).call(environment)
131
+ trajectory.session_id = result.id
132
+ store.save_artifact(result, JSON.pretty_generate(trajectory.to_atif), path: "trajectory.json")
144
133
  end
145
134
 
146
- def write_result(started_at:, reward:, outcome:, usage:)
147
- finished_at = Time.now.utc
148
- result = {
149
- trial: id,
150
- task: task.name,
151
- agent: agent_name,
152
- model: model || bench.agent.model,
153
- reward: outcome.scored? ? reward : nil,
154
- outcome: outcome.to_h,
155
- usage: usage&.to_h,
156
- lemans_version: VERSION,
157
- # The digest already hashes the bench's shipped files along with its
158
- # config, so the per-file listing added bulk, not pinning.
159
- profile_digest: bench.digest,
160
- task_digest: task.digest,
161
- bench: bench.revision.to_h,
162
- started_at: started_at.iso8601,
163
- finished_at: finished_at.iso8601,
164
- duration_sec: (finished_at - started_at).round(1),
165
- # Where the wall clock went: without this, a slow sandbox morning
166
- # reads as a slow model.
167
- phases: @phases,
168
- tags: task.tags,
169
- metadata: task.metadata
170
- }
171
- atomic_write(result_path, "#{JSON.pretty_generate(result)}\n")
172
- result
173
- end
135
+ def check_cost_limit!
136
+ limit = config.agent.cost_limit
137
+ cost = result.usage&.cost_usd
138
+ return unless limit && cost && result.scored? && cost > limit
174
139
 
175
- def result_path = dir.join("result.json")
140
+ result.completed!(
141
+ Result::Outcome.new(:cost_ceiling_reached, format("spent $%<cost>.4f against a $%<limit>.4f limit", cost:, limit:)),
142
+ result.usage
143
+ )
144
+ end
176
145
 
177
- # --resume treats any result.json as a finished attempt, so the write must
178
- # be atomic: a rename is either all there or not there at all.
179
- def atomic_write(path, content)
180
- tmp = path.dirname.join(".#{path.basename}.#{Process.pid}.#{SecureRandom.hex(4)}")
181
- tmp.write(content)
182
- tmp.rename(path)
146
+ def phase(name)
147
+ result.phase_started(name)
148
+ yield
183
149
  ensure
184
- tmp&.delete if tmp&.exist?
150
+ result.phase_finished(name)
185
151
  end
186
152
  end
187
153
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lemans
4
- VERSION = "0.2.2"
4
+ VERSION = "1.0.0.pre.1"
5
5
  end
data/lib/lemans.rb CHANGED
@@ -3,11 +3,12 @@
3
3
  require "zeitwerk"
4
4
 
5
5
  loader = Zeitwerk::Loader.for_gem
6
- loader.inflector.inflect("cli" => "CLI", "atif" => "ATIF")
6
+ loader.inflector.inflect("cli" => "CLI", "atif" => "ATIF", "fs" => "FS",
7
+ "sdk" => "SDK", "sdk_tweaks" => "SDKTweaks")
7
8
  # miniswen lives in this repo but is a plain-require gem of its own.
8
9
  loader.ignore("#{__dir__}/miniswen.rb", "#{__dir__}/miniswen")
9
10
  # assets/ holds files uploaded into sandboxes, not Ruby the harness loads.
10
- loader.ignore("#{__dir__}/lemans/verifier/assets")
11
+ loader.ignore("#{__dir__}/lemans/trial/verifier/assets")
11
12
  loader.setup
12
13
 
13
14
  require "miniswen"
@@ -17,6 +17,8 @@ module Miniswen
17
17
  new(result: result, model: model, session_id: session_id, agent: agent)
18
18
  end
19
19
 
20
+ attr_writer :session_id
21
+
20
22
  def initialize(result:, model:, session_id: nil, agent: {})
21
23
  @result = result
22
24
  @model = model
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: 0.2.2
4
+ version: 1.0.0.pre.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Svyatoslav Kryukov
@@ -52,6 +52,34 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0.203'
55
+ - !ruby/object:Gem::Dependency
56
+ name: faraday
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.10'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.10'
69
+ - !ruby/object:Gem::Dependency
70
+ name: faraday-multipart
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.1'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.1'
55
83
  - !ruby/object:Gem::Dependency
56
84
  name: rexml
57
85
  requirement: !ruby/object:Gem::Requirement
@@ -124,45 +152,50 @@ files:
124
152
  - exe/lemans
125
153
  - exe/lemans-remote
126
154
  - lib/lemans.rb
155
+ - lib/lemans/agent.rb
127
156
  - lib/lemans/agents.rb
128
- - lib/lemans/agents/base.rb
129
157
  - lib/lemans/agents/miniswen.rb
130
158
  - lib/lemans/agents/miniswen_installed.rb
131
159
  - lib/lemans/agents/nop.rb
132
160
  - lib/lemans/agents/oracle.rb
133
- - lib/lemans/bench.rb
134
161
  - lib/lemans/cli.rb
135
162
  - lib/lemans/cli/board_reporter.rb
136
163
  - lib/lemans/cli/progress_reporter.rb
164
+ - lib/lemans/cli/report.rb
165
+ - lib/lemans/cli/report/aggregate.rb
137
166
  - lib/lemans/clobber.rb
167
+ - lib/lemans/config.rb
168
+ - lib/lemans/config/agent.rb
169
+ - lib/lemans/config/conversion.rb
170
+ - lib/lemans/config/environment.rb
171
+ - lib/lemans/config/image_spec.rb
172
+ - lib/lemans/config/network_policy.rb
173
+ - lib/lemans/config/revision.rb
174
+ - lib/lemans/config/setup.rb
175
+ - lib/lemans/config/tree_digest.rb
176
+ - lib/lemans/config/verifier.rb
177
+ - lib/lemans/environment.rb
138
178
  - lib/lemans/environments.rb
139
- - lib/lemans/environments/base.rb
140
179
  - lib/lemans/environments/daytona.rb
180
+ - lib/lemans/environments/daytona/faraday_transfer.rb
141
181
  - lib/lemans/environments/daytona/retries.rb
142
182
  - lib/lemans/environments/daytona/sdk_tweaks.rb
143
183
  - lib/lemans/environments/daytona/shell.rb
144
184
  - lib/lemans/environments/daytona/snapshot_store.rb
145
- - lib/lemans/network_policy.rb
146
- - lib/lemans/patch.rb
147
- - lib/lemans/restore_paths.rb
148
- - lib/lemans/results/aggregate.rb
149
- - lib/lemans/results/cost_source.rb
150
- - lib/lemans/results/outcome.rb
151
- - lib/lemans/results/report.rb
152
- - lib/lemans/results/sorting.rb
153
- - lib/lemans/results/tally.rb
154
- - lib/lemans/results/usage.rb
155
- - lib/lemans/run.rb
156
- - lib/lemans/setup.rb
157
- - lib/lemans/setup_files.rb
158
- - lib/lemans/snapshot.rb
159
- - lib/lemans/task.rb
160
- - lib/lemans/tree_digest.rb
185
+ - lib/lemans/result.rb
186
+ - lib/lemans/runner.rb
187
+ - lib/lemans/runner/executor.rb
188
+ - lib/lemans/runner/task.rb
189
+ - lib/lemans/store.rb
190
+ - lib/lemans/stores/fs.rb
191
+ - lib/lemans/task_definition.rb
161
192
  - lib/lemans/trial.rb
162
- - lib/lemans/units.rb
163
- - lib/lemans/verifier.rb
164
- - lib/lemans/verifier/assets/eport-lemans.rb
165
- - lib/lemans/verifier/assets/lemans_minitest_reporter.rb
193
+ - lib/lemans/trial/patch.rb
194
+ - lib/lemans/trial/setup.rb
195
+ - lib/lemans/trial/snapshot.rb
196
+ - lib/lemans/trial/verifier.rb
197
+ - lib/lemans/trial/verifier/assets/eport-lemans.rb
198
+ - lib/lemans/trial/verifier/assets/lemans_minitest_reporter.rb
166
199
  - lib/lemans/version.rb
167
200
  - lib/miniswen.rb
168
201
  - lib/miniswen/agent.rb
@@ -190,7 +223,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
190
223
  requirements:
191
224
  - - ">="
192
225
  - !ruby/object:Gem::Version
193
- version: 3.3.0
226
+ version: 3.4.0
194
227
  required_rubygems_version: !ruby/object:Gem::Requirement
195
228
  requirements:
196
229
  - - ">="
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Lemans
4
- module Agents
5
- # What the harness asks of an agent: install yourself, then work on the
6
- # instruction. The name-to-class registry lives on the Agents module.
7
- class Base
8
- Result = Data.define(:outcome, :usage, :trajectory)
9
-
10
- attr_reader :profile, :model
11
-
12
- def initialize(profile:, model: nil)
13
- @profile = profile
14
- @model = model || profile.model
15
- end
16
-
17
- def name = self.class::NAME
18
-
19
- # Run before the agent phase's network policy narrows, so an agent that
20
- # pulls its own runtime can still reach a package index.
21
- def install(_environment, task:) = nil # rubocop:disable Lint/UnusedMethodArgument
22
-
23
- def call(environment, task:, logs_dir:) = raise(NotImplementedError)
24
-
25
- private
26
-
27
- def timeout_sec = profile.timeout_sec
28
- end
29
- end
30
- end