miniswen 1.0.0 → 1.2.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/README.md +15 -3
- data/lib/miniswen/agent.rb +12 -6
- data/lib/miniswen/cli/reporter.rb +104 -0
- data/lib/miniswen/cli.rb +36 -82
- data/lib/miniswen/environment/docker.rb +69 -0
- data/lib/miniswen/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: de70dca3df1d5d69b884c838142ecf6143714e4160df12cd70ffd9d82d9cd823
|
|
4
|
+
data.tar.gz: 9fc776f43acb75cd49b0b02a45dbaf2017927c2a5a9d0ac53589925b5be6cb99
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b65975d0c54ef1588a896917dc7ee86b1c87c7a6c750d0354e6d1d8681743e20c2561890ebeefbce3cae2d96981ac5b41c26a6411b2d350653bce026ca9e730c
|
|
7
|
+
data.tar.gz: c067f93043fd5f056a243200b08e5d92bd3c4d25356bc961ae8a377e2dc86ef91600ce9d468747f5b18daa746be2f50e87122f5ad9ea4de6827b0fb1e1727b84
|
data/README.md
CHANGED
|
@@ -46,7 +46,7 @@ my-bench/
|
|
|
46
46
|
├── hello-world/
|
|
47
47
|
│ ├── instruction.md # what the agent is asked to do; YAML frontmatter carries
|
|
48
48
|
│ │ # name, description, difficulty, tags, metadata — plus the
|
|
49
|
-
│ │ # per-task overrides: setup, restore, verifier.setup
|
|
49
|
+
│ │ # per-task overrides: setup, restore, verifier.setup, environment
|
|
50
50
|
│ ├── environment/Dockerfile # [optional] a task-specific sandbox image, instead of the shared one
|
|
51
51
|
│ ├── environment.patch # [optional] task setup patch: applied and resealed as a fresh git repo at setup
|
|
52
52
|
│ ├── verification_test.rb # grades the result
|
|
@@ -59,6 +59,9 @@ The `bench.yml` looks like this:
|
|
|
59
59
|
```yaml
|
|
60
60
|
version: 1
|
|
61
61
|
|
|
62
|
+
# inherit_from: ../bench.yml # [optional] start from another bench.yml; the sections below are
|
|
63
|
+
# # deep-merged over it (lists replace; set a key to ~ to drop it)
|
|
64
|
+
|
|
62
65
|
# setup: # [optional] sandbox preparation, run before the agent starts
|
|
63
66
|
# files: [fixtures/seed.sql] # uploaded for the commands to consume, then wiped
|
|
64
67
|
# commands: [bin/sandbox-setup]
|
|
@@ -68,6 +71,11 @@ environment:
|
|
|
68
71
|
# backend: "daytona" # or "docker"
|
|
69
72
|
# dockerfile: environment/Dockerfile # the default — or pin a published image instead:
|
|
70
73
|
# image: ghcr.io/acme/my-bench@sha256:...
|
|
74
|
+
# profiles: # [optional] named alternatives to the shared image; a task
|
|
75
|
+
# campfire: # picks one with `environment: campfire` in its frontmatter
|
|
76
|
+
# dockerfile: docker/campfire/Dockerfile
|
|
77
|
+
# fizzy:
|
|
78
|
+
# image: ghcr.io/acme/fizzy@sha256:...
|
|
71
79
|
resources: { cpus: 2, memory: 2GB, storage: 5GB }
|
|
72
80
|
build_timeout: 10m
|
|
73
81
|
network:
|
|
@@ -76,6 +84,7 @@ environment:
|
|
|
76
84
|
agent:
|
|
77
85
|
name: miniswen-installed
|
|
78
86
|
model: openrouter/z-ai/glm-5.2 # any model RubyLLM can reach; a list benchmarks several in one run
|
|
87
|
+
# append #<effort> (e.g. openrouter/openai/gpt-5.6-luna#xhigh) to pin the reasoning effort
|
|
79
88
|
timeout: 30m
|
|
80
89
|
step_limit: 100
|
|
81
90
|
cost_limit: 5.0
|
|
@@ -133,7 +142,7 @@ A minimal task example—checking whether an agent can write "Hello, world" into
|
|
|
133
142
|
+Hello, world
|
|
134
143
|
```
|
|
135
144
|
|
|
136
|
-
- `verification_test.rb`: a Ruby test that grades the solution (pass → 1, fail → 0; for partial credit, write a float in 0.0..1.0 to `$LOGS/reward.txt` instead). We use Minitest:
|
|
145
|
+
- `verification_test.rb`: a Ruby test that grades the solution (pass → 1, fail → 0; for partial credit, write a float in 0.0..1.0 to `$LOGS/reward.txt` instead). A check worth recording but not grading goes into `allow_failure { ... }` (`include LemansReport::Assertions`): a failed assertion inside lands in `checks.json` as `fail (allowed)` with its message and leaves the reward alone, while errors and skips stay hard failures. We use Minitest:
|
|
137
146
|
|
|
138
147
|
```ruby
|
|
139
148
|
require "minitest/autorun"
|
|
@@ -161,13 +170,16 @@ verifier:
|
|
|
161
170
|
|
|
162
171
|
An `environment.patch` next to `instruction.md` is always applied, declared or not.
|
|
163
172
|
|
|
173
|
+
A heavy task may need more than the bench-wide budgets: a `bench.yml` in the task directory is deep-merged over the bench's (as if it had `inherit_from` pointing at it) and applies to that task's trials only — say, `agent: { timeout: 2h, step_limit: 300 }`.
|
|
174
|
+
|
|
164
175
|
### 3. Set credentials
|
|
165
176
|
|
|
166
177
|
```bash
|
|
167
178
|
export DAYTONA_API_KEY=... # or DAYTONA_TOKEN (if using Daytona)
|
|
168
179
|
export OPENROUTER_API_KEY=... # or ANTHROPIC_API_KEY, OPENAI_API_KEY, ... — matching your model
|
|
169
180
|
|
|
170
|
-
export LEMANS_PROVIDER_ORDER="
|
|
181
|
+
export LEMANS_PROVIDER_ORDER="chutes" # [optional] Pin an OpenRouter model to provider slugs (alias: OPENROUTER_PROVIDER_ORDER)
|
|
182
|
+
export LEMANS_PROVIDER_ORDER="z-ai,chutes" # ...or try them in order
|
|
171
183
|
```
|
|
172
184
|
|
|
173
185
|
### 4. Prove the bench before benchmarking anything
|
data/lib/miniswen/agent.rb
CHANGED
|
@@ -232,11 +232,14 @@ module Miniswen
|
|
|
232
232
|
private attr_reader :max_steps, :max_time, :max_cost, :exec_timeout,
|
|
233
233
|
:clock, :reporter
|
|
234
234
|
|
|
235
|
-
# `model` is a litellm-style name ("openrouter/z-ai/glm-5.2")
|
|
235
|
+
# `model` is a litellm-style name ("openrouter/z-ai/glm-5.2"), optionally
|
|
236
|
+
# suffixed with a reasoning effort ("openrouter/openai/gpt-5.6-luna#xhigh").
|
|
237
|
+
# Limits of 0 or nil are disabled.
|
|
236
238
|
def initialize(model:, environment:, max_steps: 0, max_time: 0, max_cost: nil,
|
|
237
239
|
exec_timeout: 30, clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) },
|
|
238
240
|
reporter: nil)
|
|
239
|
-
|
|
241
|
+
name, @effort = model.split("#", 2)
|
|
242
|
+
@provider, @id = name.split("/", 2)
|
|
240
243
|
unless @id
|
|
241
244
|
@id = @provider
|
|
242
245
|
@provider = nil
|
|
@@ -315,8 +318,8 @@ module Miniswen
|
|
|
315
318
|
_, provider = resolved
|
|
316
319
|
env = provider.configuration_requirements.to_h { [ it.to_s.upcase, RubyLLM.config.public_send(it) ] }.compact
|
|
317
320
|
|
|
318
|
-
order =
|
|
319
|
-
env["
|
|
321
|
+
order = provider_order
|
|
322
|
+
env["OPENROUTER_PROVIDER_ORDER"] = order if order
|
|
320
323
|
env
|
|
321
324
|
end
|
|
322
325
|
|
|
@@ -500,7 +503,8 @@ module Miniswen
|
|
|
500
503
|
tools: { bash: @bash_tool },
|
|
501
504
|
temperature: nil,
|
|
502
505
|
model: model_info,
|
|
503
|
-
params: routing_params
|
|
506
|
+
params: routing_params,
|
|
507
|
+
thinking: (RubyLLM::Thinking::Config.new(effort: @effort) if @effort)
|
|
504
508
|
)
|
|
505
509
|
payload(response)
|
|
506
510
|
rescue RubyLLM::Error => e
|
|
@@ -531,12 +535,14 @@ module Miniswen
|
|
|
531
535
|
end
|
|
532
536
|
|
|
533
537
|
def routing_params
|
|
534
|
-
order =
|
|
538
|
+
order = provider_order
|
|
535
539
|
return {} unless order && @provider == "openrouter"
|
|
536
540
|
|
|
537
541
|
{ provider: { order: order.split(",").map(&:strip), allow_fallbacks: false } }
|
|
538
542
|
end
|
|
539
543
|
|
|
544
|
+
def provider_order = ENV["LEMANS_PROVIDER_ORDER"] || ENV["OPENROUTER_PROVIDER_ORDER"]
|
|
545
|
+
|
|
540
546
|
def cost_source
|
|
541
547
|
if local?
|
|
542
548
|
return CostSource.new(name: :local_provider, model: @model,
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Miniswen
|
|
4
|
+
class CLI
|
|
5
|
+
# Prints messages and tool calls in real-time. The renderer deliberately
|
|
6
|
+
# keeps the captured (non-TTY) version plain, which makes it useful in CI
|
|
7
|
+
# and when piping a run to a log file too.
|
|
8
|
+
class Reporter
|
|
9
|
+
private attr_reader :io
|
|
10
|
+
|
|
11
|
+
# Tool output can be extremely noisy (for example, a recursive grep or
|
|
12
|
+
# a test runner dumping a log). Keep the normal report useful while
|
|
13
|
+
# allowing -vv to retain the complete output for debugging.
|
|
14
|
+
MAX_TOOL_OUTPUT_CHARS = 1_000
|
|
15
|
+
|
|
16
|
+
def initialize(io = $stdout, verbose: false, tool_output: verbose, reasoning: true)
|
|
17
|
+
@io = io
|
|
18
|
+
@verbose = verbose
|
|
19
|
+
@tool_output = tool_output
|
|
20
|
+
@reasoning = reasoning
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def on_message(message)
|
|
24
|
+
case message[:role].to_s
|
|
25
|
+
when "assistant"
|
|
26
|
+
write_assistant(message)
|
|
27
|
+
when "tool"
|
|
28
|
+
write_tool(message)
|
|
29
|
+
when "user"
|
|
30
|
+
write_block("!", message[:content], :warning)
|
|
31
|
+
else
|
|
32
|
+
write_block("·", message[:content], :muted)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Tool calls are reported separately so the command is visible before
|
|
37
|
+
# its output arrives. It is not added to the trajectory sent to the LLM.
|
|
38
|
+
def on_tool_call(call)
|
|
39
|
+
command = call.dig(:arguments, "command") || call.dig(:arguments, :command)
|
|
40
|
+
return if command.to_s.empty?
|
|
41
|
+
|
|
42
|
+
line = style("$ #{command}", :command)
|
|
43
|
+
io.puts(" #{line}")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def print_summary(result)
|
|
47
|
+
write_assistant(result.messages.last)
|
|
48
|
+
write_block("↳", "steps=#{result.steps} · cost=$#{result.cost_usd}", :muted)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def print_failure(result)
|
|
52
|
+
write_block("!", "Miniswen failed: #{result.status}", :warning)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def write_tool(message)
|
|
58
|
+
return write_block("↳", message[:content], :tool) if @tool_output
|
|
59
|
+
|
|
60
|
+
exit_code = message.dig(:observation, :exit_code)
|
|
61
|
+
return if exit_code.nil?
|
|
62
|
+
|
|
63
|
+
if exit_code.zero?
|
|
64
|
+
write_block("↳", "ok", :muted)
|
|
65
|
+
else
|
|
66
|
+
output = truncate(message.dig(:observation, :output).to_s.strip)
|
|
67
|
+
write_block("!", "not ok: exit #{exit_code}\n#{output}", :warning)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def write_assistant(message)
|
|
72
|
+
content = message[:content].to_s.strip
|
|
73
|
+
write_block("∴", message[:thinking], :thinking) if @reasoning && (@verbose || content.empty?)
|
|
74
|
+
write_block("●", content, :assistant)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def write_block(marker, content, tone)
|
|
78
|
+
text = content.to_s.strip
|
|
79
|
+
return if text.empty?
|
|
80
|
+
|
|
81
|
+
text = truncate(text) if %i[tool thinking].include?(tone)
|
|
82
|
+
lines = text.lines(chomp: true)
|
|
83
|
+
io.puts("#{style(marker, tone)} #{style(lines.shift, tone)}")
|
|
84
|
+
lines.each { |line| io.puts(" #{style(line, tone)}") }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def truncate(text)
|
|
88
|
+
return text if @verbose || text.length <= MAX_TOOL_OUTPUT_CHARS
|
|
89
|
+
|
|
90
|
+
head = MAX_TOOL_OUTPUT_CHARS / 2
|
|
91
|
+
tail = MAX_TOOL_OUTPUT_CHARS - head
|
|
92
|
+
omitted = text.length - head - tail
|
|
93
|
+
"#{text[0, head]}\n... [#{omitted} characters omitted] ...\n#{text[-tail, tail]}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def style(text, tone)
|
|
97
|
+
return text unless io.respond_to?(:tty?) && io.tty?
|
|
98
|
+
|
|
99
|
+
colors = { assistant: 36, tool: 32, warning: 33, command: 35, muted: 90, thinking: 90 }
|
|
100
|
+
"\e[#{colors.fetch(tone)}m#{text}\e[0m"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
data/lib/miniswen/cli.rb
CHANGED
|
@@ -1,88 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "optparse"
|
|
4
|
+
require "fileutils"
|
|
4
5
|
|
|
5
6
|
require "miniswen/version"
|
|
7
|
+
require "miniswen/cli/reporter"
|
|
6
8
|
|
|
7
9
|
module Miniswen
|
|
8
10
|
class CLI # :nodoc:
|
|
9
|
-
# Prints messages and tool calls in real-time. The renderer deliberately
|
|
10
|
-
# keeps the captured (non-TTY) version plain, which makes it useful in CI
|
|
11
|
-
# and when piping a run to a log file too.
|
|
12
|
-
class Reporter
|
|
13
|
-
private attr_reader :io
|
|
14
|
-
|
|
15
|
-
# Tool output can be extremely noisy (for example, a recursive grep or
|
|
16
|
-
# a test runner dumping a log). Keep the normal report useful while
|
|
17
|
-
# allowing -vv to retain the complete output for debugging.
|
|
18
|
-
MAX_TOOL_OUTPUT_CHARS = 1_000
|
|
19
|
-
|
|
20
|
-
def initialize(io = $stdout, verbose: false)
|
|
21
|
-
@io = io
|
|
22
|
-
@verbose = verbose
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def on_message(message)
|
|
26
|
-
case message[:role].to_s
|
|
27
|
-
when "assistant"
|
|
28
|
-
write_block("●", message[:content], :assistant)
|
|
29
|
-
when "tool"
|
|
30
|
-
write_block("↳", message[:content], :tool)
|
|
31
|
-
when "user"
|
|
32
|
-
write_block("!", message[:content], :warning)
|
|
33
|
-
else
|
|
34
|
-
write_block("·", message[:content], :muted)
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Tool calls are reported separately so the command is visible before
|
|
39
|
-
# its output arrives. It is not added to the trajectory sent to the LLM.
|
|
40
|
-
def on_tool_call(call)
|
|
41
|
-
command = call.dig(:arguments, "command") || call.dig(:arguments, :command)
|
|
42
|
-
return if command.to_s.empty?
|
|
43
|
-
|
|
44
|
-
line = style("$ #{command}", :command)
|
|
45
|
-
io.puts(" #{line}")
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def print_summary(result)
|
|
49
|
-
write_block("●", result.messages.last[:content], :assistant)
|
|
50
|
-
write_block("↳", "steps=#{result.steps} · cost=$#{result.cost_usd}", :muted)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def print_failure(result)
|
|
54
|
-
write_block("!", "Miniswen failed: #{result.status}", :warning)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
private
|
|
58
|
-
|
|
59
|
-
def write_block(marker, content, tone)
|
|
60
|
-
text = content.to_s.strip
|
|
61
|
-
return if text.empty?
|
|
62
|
-
|
|
63
|
-
text = truncate_tool_output(text) if tone == :tool
|
|
64
|
-
lines = text.lines(chomp: true)
|
|
65
|
-
io.puts("#{style(marker, tone)} #{style(lines.shift, tone)}")
|
|
66
|
-
lines.each { |line| io.puts(" #{style(line, tone)}") }
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def truncate_tool_output(text)
|
|
70
|
-
return text if @verbose || text.length <= MAX_TOOL_OUTPUT_CHARS
|
|
71
|
-
|
|
72
|
-
head = MAX_TOOL_OUTPUT_CHARS / 2
|
|
73
|
-
tail = MAX_TOOL_OUTPUT_CHARS - head
|
|
74
|
-
omitted = text.length - head - tail
|
|
75
|
-
"#{text[0, head]}\n... [#{omitted} characters omitted] ...\n#{text[-tail, tail]}"
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def style(text, tone)
|
|
79
|
-
return text unless io.respond_to?(:tty?) && io.tty?
|
|
80
|
-
|
|
81
|
-
colors = { assistant: 36, tool: 32, warning: 33, command: 35, muted: 90 }
|
|
82
|
-
"\e[#{colors.fetch(tone)}m#{text}\e[0m"
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
11
|
attr_reader :instruction, :model, :options
|
|
87
12
|
|
|
88
13
|
def initialize
|
|
@@ -90,6 +15,8 @@ module Miniswen
|
|
|
90
15
|
@model = ENV.fetch("MINISWEN_MODEL", nil)
|
|
91
16
|
@options = {}
|
|
92
17
|
@verbose = false
|
|
18
|
+
@show_output = false
|
|
19
|
+
@reasoning = true
|
|
93
20
|
@quiet = false
|
|
94
21
|
@results_path = nil
|
|
95
22
|
@atif_path = nil
|
|
@@ -110,8 +37,16 @@ module Miniswen
|
|
|
110
37
|
|
|
111
38
|
require "miniswen/local"
|
|
112
39
|
|
|
113
|
-
reporter = @quiet ? nil : Reporter.new(verbose: @verbose)
|
|
114
|
-
|
|
40
|
+
reporter = @quiet ? nil : Reporter.new(verbose: @verbose, tool_output: @verbose || @show_output, reasoning: @reasoning)
|
|
41
|
+
environment =
|
|
42
|
+
if @docker_id
|
|
43
|
+
require "miniswen/environment/docker"
|
|
44
|
+
Environment::Docker.new(@docker_id)
|
|
45
|
+
else
|
|
46
|
+
Local.new
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
agent = Agent.new(model:, reporter:, environment:, **options)
|
|
115
50
|
|
|
116
51
|
begin
|
|
117
52
|
result = agent.run(instruction)
|
|
@@ -133,8 +68,15 @@ module Miniswen
|
|
|
133
68
|
private
|
|
134
69
|
|
|
135
70
|
def write_results(result)
|
|
136
|
-
|
|
137
|
-
|
|
71
|
+
if @results_path
|
|
72
|
+
FileUtils.mkdir_p(File.dirname(@results_path))
|
|
73
|
+
File.write(@results_path, JSON.generate(result.to_h))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
if @atif_path
|
|
77
|
+
FileUtils.mkdir_p(File.dirname(@atif_path))
|
|
78
|
+
write_atif(result)
|
|
79
|
+
end
|
|
138
80
|
end
|
|
139
81
|
|
|
140
82
|
def error_message(error)
|
|
@@ -162,7 +104,7 @@ module Miniswen
|
|
|
162
104
|
end
|
|
163
105
|
|
|
164
106
|
opts.on("-p INSTRUCTION", "--prompt=INSTRUCTION", String, "Instruction prompt") do |v|
|
|
165
|
-
@instruction = v
|
|
107
|
+
@instruction = File.file?(v) ? File.read(v) : v
|
|
166
108
|
end
|
|
167
109
|
|
|
168
110
|
opts.on("--max-steps=STEPS", Integer, "Max steps count") do |v|
|
|
@@ -185,6 +127,14 @@ module Miniswen
|
|
|
185
127
|
@quiet = true
|
|
186
128
|
end
|
|
187
129
|
|
|
130
|
+
opts.on("--show-output", "Print tool output instead of just the exit status") do
|
|
131
|
+
@show_output = true
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
opts.on("--no-reasoning", "Hide the model's reasoning") do
|
|
135
|
+
@reasoning = false
|
|
136
|
+
end
|
|
137
|
+
|
|
188
138
|
opts.on("--results-path=PATH", String, "Write the run result as JSON to PATH") do |v|
|
|
189
139
|
@results_path = v
|
|
190
140
|
end
|
|
@@ -193,6 +143,10 @@ module Miniswen
|
|
|
193
143
|
@atif_path = v
|
|
194
144
|
end
|
|
195
145
|
|
|
146
|
+
opts.on("--docker=ID", String, "Docker container ID to exec commands on") do |v|
|
|
147
|
+
@docker_id = v
|
|
148
|
+
end
|
|
149
|
+
|
|
196
150
|
opts.on("--refresh-registry", "Refresh the model registry, persist it, and exit") do
|
|
197
151
|
@refresh_registry = true
|
|
198
152
|
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
require "miniswen/environment"
|
|
6
|
+
|
|
7
|
+
module Miniswen
|
|
8
|
+
class Environment
|
|
9
|
+
class Docker < self
|
|
10
|
+
TIMEOUT_MARKED_EXIT_CODES = [ 124, 143 ].freeze
|
|
11
|
+
|
|
12
|
+
private attr_reader :id
|
|
13
|
+
|
|
14
|
+
def initialize(id)
|
|
15
|
+
@id = id
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def exec(command, timeout: nil, env: nil)
|
|
19
|
+
argv = [ "docker", "exec" ]
|
|
20
|
+
env&.each { |key, value| argv += [ "--env", "#{key}=#{value}" ] }
|
|
21
|
+
argv << id
|
|
22
|
+
argv += [ "timeout", timeout.ceil.to_s ] if timeout&.positive?
|
|
23
|
+
argv += [ "sh", "-c", command ]
|
|
24
|
+
|
|
25
|
+
Open3.popen2e(*argv) do |stdin, pipe, wait|
|
|
26
|
+
stdin.close
|
|
27
|
+
deadline = (now + timeout + 10 if timeout&.positive?) # add some slack
|
|
28
|
+
output = +""
|
|
29
|
+
timed_out = false
|
|
30
|
+
|
|
31
|
+
loop do
|
|
32
|
+
remaining = deadline && deadline - now
|
|
33
|
+
if remaining && remaining <= 0
|
|
34
|
+
timed_out = true
|
|
35
|
+
kill(wait.pid)
|
|
36
|
+
break
|
|
37
|
+
end
|
|
38
|
+
next unless pipe.wait_readable(remaining)
|
|
39
|
+
|
|
40
|
+
chunk = pipe.read_nonblock(65_536, exception: false)
|
|
41
|
+
break if chunk.nil?
|
|
42
|
+
next if chunk == :wait_readable
|
|
43
|
+
|
|
44
|
+
output << chunk
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
status = wait.value
|
|
48
|
+
exit_code = timed_out ? 124 : (status.exitstatus || 1)
|
|
49
|
+
output = output.force_encoding(Encoding::UTF_8).scrub
|
|
50
|
+
if timeout&.positive? && (timed_out || TIMEOUT_MARKED_EXIT_CODES.include?(exit_code))
|
|
51
|
+
output = "#{output}\n<command timed out after #{timeout} seconds>"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
ExecResult.new(exit_code:, output:)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def kill(pid)
|
|
61
|
+
Process.kill("KILL", pid)
|
|
62
|
+
rescue Errno::ESRCH
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
data/lib/miniswen/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: miniswen
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Svyatoslav Kryukov
|
|
@@ -55,7 +55,9 @@ files:
|
|
|
55
55
|
- lib/miniswen.rb
|
|
56
56
|
- lib/miniswen/agent.rb
|
|
57
57
|
- lib/miniswen/cli.rb
|
|
58
|
+
- lib/miniswen/cli/reporter.rb
|
|
58
59
|
- lib/miniswen/environment.rb
|
|
60
|
+
- lib/miniswen/environment/docker.rb
|
|
59
61
|
- lib/miniswen/local.rb
|
|
60
62
|
- lib/miniswen/ruby_llm.rb
|
|
61
63
|
- lib/miniswen/testing.rb
|