miniswen 1.4.0 → 1.5.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 49e7b2a4a062867c6b85476f94ebda50ce7d356add5500d40b2c5c513155f7ca
4
- data.tar.gz: 4b5dfc61e161b9e2776b2547e8a10862e785fedcbd5fff1a1621054657ed21fb
3
+ metadata.gz: b72b4a7a4e82cc48a0a50e7a27c338f34685ee9e6fefdfc555213b8c5d8b2475
4
+ data.tar.gz: 3d309e32ffc77990653f49768ee1dd1e6c581b56a93eddebd61f37d8ac9739dc
5
5
  SHA512:
6
- metadata.gz: b6e67519d94b5e32469173068f8121f8682dd5f16398c38c118f4516931d9e8b2e068e89f612bdc3d543df0a10cc9d8279bc425de404b1e63774085f77f8fab1
7
- data.tar.gz: addc1fbee5c18257a2e0f115316a33a4f0f5e9233bb6a7f47ed43d244f1e49ec0a4fd912519e9442edb149a01c25d21cebe3f84cf8d4d6142d170bc30266c80b
6
+ metadata.gz: 2fd6ae2583048739c94ee8dc70e92c214f4163cb7a86e2201e450d14549abc3cb5d92aba5f1f3c6c1a934968f1b738f3d8561392a2523412393cf9f0c33e04d9
7
+ data.tar.gz: 89ae33237c77e4025e9fe4bf5a5c5036b3098f32f9e32f91a4bd2d0925cd61cc366bd58a3de3b182b0fa4f31f6e0027a674f5b1d78a072cd2905bc0d1d3d52f5
data/README.md CHANGED
@@ -6,6 +6,7 @@ lemans is a harness for benchmarking coding agents, the Ruby way:
6
6
  - **Conventional**, aka _boilerplate-free_: an instruction, a Docker environment, and a test script — that's all you need to describe an eval
7
7
  - **Trustworthy**: a grade can't be gamed by the agent, an infrastructure failure can't masquerade as a model failure, and every result carries enough digests to prove what it actually measured
8
8
  - Powered by **miniswen**, a Ruby version of mini-swe-agent (powered by [RubyLLM](https://rubyllm.com/), so it works with any LLM) and [Daytona](https://www.daytona.io) sandboxes.
9
+ - **Extensible**: load a custom agent or a sandboxing backend on-the-fly defined in a Ruby file or provided by a gem.
9
10
 
10
11
  > [!TIP]
11
12
  > Check [Rails AI Evals](https://github.com/rails/ai-evals) for a full-featured example.
@@ -15,6 +16,7 @@ lemans is a harness for benchmarking coding agents, the Ruby way:
15
16
  - Ruby 3.4+ is required to run `lemans`
16
17
  - Daytona account (API token) or Docker (for local sandboxes)
17
18
  - Some LLM provider/proxy credentials (e.g., OpenRouter)
19
+ - Bash in the sandbox image for command execution
18
20
  - iproute2 in the sandbox image, so a jailed agent keeps loopback while cut off the internet
19
21
 
20
22
  ## Getting started
@@ -102,6 +104,8 @@ verifier:
102
104
  # restore: [test, bin] # [optional] paths restored from the pre-agent snapshot, so grading can't be tampered with
103
105
  ```
104
106
 
107
+ On the Docker backend, `allowlist` sends traffic through a proxy container. Only tools that use `http_proxy`/`https_proxy` can reach the listed hosts. Host names must match exactly, and IP ranges are not supported. See [the proxy README](lib/lemans/environments/docker/proxy/README.md).
108
+
105
109
  A minimal task example—checking whether an agent can write "Hello, world" into a file:
106
110
 
107
111
  - `instruction.md`: the task itself (note the preamble about the training corpora and the frontmatter)
@@ -231,6 +235,17 @@ Each trial writes a flat `runs/<model>/<task>__<id>/` directory:
231
235
 
232
236
  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.
233
237
 
238
+ `lemans restart <run>` continues a multistep run that failed for reasons outside the model, such as a provider outage. Give it the run directories or the trial ids: `lemans restart RUN [RUN...]` restarts every run under the same options, `-c` at once, and runs none if any is refused. The new run replays the agent patches of the settled steps in a fresh sandbox and starts at the next step. It copies the settled steps' usage, phases, and artifacts, and records the source in `restarted_from`. The failed run stays as it is. It restarts even if the task or the bench changed since; the new run records the current digests.
239
+
240
+ Two modes restart inside a step:
241
+
242
+ - `--recover` continues the failed step's agent session. The new run also replays the step's partial patch, and the agent goes on from the saved `agent.result.json`. The session keeps the steps, tokens, cost, and time it already spent. Only `miniswen` and `miniswen-installed` can recover.
243
+ - `--reverify` runs the last verification again with the current tests. Use it after you change the grading or the harness. The new run replays the graded step's patch and does not run its agent. If the verification was an intermediate gate and it now passes, the trial goes on to the next step. A reverification accepts scored runs.
244
+
245
+ A scored run restarts in the other modes only with `--allow-scored`.
246
+
247
+ Only the files that git tracks come back: ignored files (databases, logs, installed dependencies) are not in the patches.
248
+
234
249
  You can also run `lemans report` with various flags to see aggregated results, e.g.:
235
250
 
236
251
  ```sh
@@ -249,6 +264,83 @@ gpt-5.6-luna ar-archive-book-access 2/2 2m 23s $0.0132 12.5 156905
249
264
  6 trials: 6 scored, 0 invalid, 6 solved (100%) · $0.0801 · pass@2 3/3 tasks (100%)
250
265
  ```
251
266
 
267
+ ### 6. Extensions
268
+
269
+ You can bring **custom agents and backends** to lemans. For that, you should:
270
+
271
+ - Write an agent or a backend implementation in a Ruby file/gem
272
+ - Load it using the `--require` / `-r` option specified **before the command**:
273
+
274
+ ```sh
275
+ lemans -r ./my-agent.rb run --agent my-agent ...
276
+ ```
277
+
278
+ - **IMPORTANT:** The extension must register itself in the corresponding registry and follow the leman's interface:
279
+
280
+ ```ruby
281
+ # my-agent.rb
282
+ class MyAgent < Lemans::Agent
283
+ NAME = "my-agent"
284
+
285
+ def install(_task, environment)
286
+ environment.upload("/path/to/my-agent", "/usr/local/bin/my-agent")
287
+ environment.exec!("chmod +x /usr/local/bin/my-agent")
288
+ end
289
+
290
+ def run(task, environment)
291
+ finished = environment.exec("cd #{task.environment.workdir} && my-agent --model #{model}", timeout:)
292
+ outcome = finished.exit_code == 124 ? :agent_timeout : :completed
293
+
294
+ # The returned object is a Lemans::Agent::Response.
295
+ # IMPORTANT: `usage` is required for correct work
296
+ Response.new(outcome: Lemans::Result::Outcome.new(outcome), usage: Lemans::Result::Usage.zero)
297
+ end
298
+ end
299
+
300
+ Lemans::Agents.register(MyAgent::NAME, MyAgent)
301
+
302
+ # my-backend.rb
303
+ class MyBackend < Lemans::Environment
304
+ NAME = "my-backend"
305
+
306
+ # Called with image:, resources:, network:, labels:, build_timeout:, and ttl: (see Lemans::Environment)
307
+ def start
308
+ @sandbox = MyCloud.create(image: image.name, cpus: resources.cpus, memory: resources.memory)
309
+ switch_network_policy!(network)
310
+ self
311
+ end
312
+
313
+ def exec(command, timeout: nil, env: {})
314
+ run = @sandbox.run(command, timeout: timeout || DEFAULT_TIMEOUT, env:)
315
+ ExecResult.new(command:, exit_code: run.exit_code, output: run.output, duration: run.duration)
316
+ end
317
+
318
+ def upload(local_path, remote_path) = @sandbox.put(local_path, remote_path)
319
+
320
+ def download(remote_path, local_path) = @sandbox.get(remote_path, local_path)
321
+
322
+ # Phases narrow the network: setup, then the agent's policy, then none for the verifier.
323
+ # IMPORTANT: raise if the policy cannot be enforced
324
+ def switch_network_policy!(policy)
325
+ @sandbox.set_network(policy.mode, hosts: policy.hosts)
326
+ @network = policy
327
+ end
328
+
329
+ def stop = @sandbox&.destroy
330
+ end
331
+
332
+ Lemans::Environments.register(MyBackend::NAME, MyBackend)
333
+ ```
334
+
335
+ ```sh
336
+ lemans -r ./my-backend.rb run --backend my-backend ...
337
+ ```
338
+
339
+ See [examples](./examples) for real-world extensions:
340
+
341
+ - [Apple Container backend](./examples/apple-container.rb)
342
+ - [Claude Code agent](./examples/claude-code.rb)
343
+
252
344
  ## CLI
253
345
 
254
346
  | Command | What it does |
@@ -256,8 +348,9 @@ gpt-5.6-luna ar-archive-book-access 2/2 2m 23s $0.0132 12.5 156905
256
348
  | `lemans init` | Scaffold a new bench directory: an annotated `bench.yml` and two example tasks |
257
349
  | `lemans tasks` | List the tasks in a bench (`--tag` to filter) |
258
350
  | `lemans run` | Run tasks and grade them (`--task`, `--tag`, `--agent`, `--model`, `--max-output-tokens`, `-k`, `-c`, `--resume`) |
259
- | `lemans report` | Summarize `runs/` as a table or CSV (`--task`, `--tag`, `--metadata key:value` to filter, `-A [task-agent-model]` to aggregate, `-S <column>` to sort); repeated attempts add pass@k per model × task, fractional grading a `credit` column |
260
- | `lemans clobber` | Delete run results (`--task`, `--ttl 10m\|2h\|1d`, `--invalid`, `-f` to skip the confirmation) |
351
+ | `lemans restart <run>...` | Continue failed multistep runs from their last settled step in new runs (`-c`, `--recover` to continue the failed step's session, `--reverify` to grade again, `--allow-scored`, `--backend`, `--max-output-tokens`) |
352
+ | `lemans report [RUNS_DIR]` | Summarize `runs/` (or `RUNS_DIR`) as a table or CSV (`--task`, `--tag`, `--model`, `--metadata key:value` to filter, `--skip-invalid` to leave out invalid trials, `-A [task-agent-model]` to aggregate, `-S <columns>` to sort, e.g. `-S score-credit`; numbers high to low, names A-Z, `^column` reverses that column; `--hide-columns steps-tokens` for a narrower table); repeated attempts add pass@k per model × task, fractional grading a `credit` column, multistep tasks a `progress` column (steps completed / task steps) |
353
+ | `lemans clobber [RUNS_DIR]` | Delete run results under `runs/` (or `RUNS_DIR`) (`--task`, `--ttl 10m\|2h\|1d`, `--invalid`, `-f` to skip the confirmation) |
261
354
 
262
355
  ## miniswen
263
356
 
@@ -273,6 +366,13 @@ $ miniswen --model 'openrouter/openai/gpt-5.6-luna' --prompt 'Write hello-world
273
366
 
274
367
  Currently, it's a one-shot agent that doesn't ask any questions. It's mostly useful for playing with eval ideas before encoding them as lemans tasks.
275
368
 
369
+ ## More tools: lemans-remote and lemans-viewer
370
+
371
+ The gem also ships two convenient (and 100% AI-maintained) CLIs:
372
+
373
+ - `lemans-remote`: scale `lemans run`-s horizontally using Daytona sandboxes; schedule tasks and pull results later
374
+ - `lemans-viewer`: a web UI for browsing trial results (if you still need a pair of human eyes to look at them)
375
+
276
376
  ## Development
277
377
 
278
378
  After checking out the repo, run `bin/setup` to install dependencies, `rake test` to run the tests, and `bin/console` for an interactive prompt.
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require "securerandom"
5
+ require "time"
4
6
  require "miniswen/version"
5
7
  require "miniswen/ruby_llm"
6
8
 
@@ -187,7 +189,9 @@ module Miniswen
187
189
 
188
190
  Result = Data.define(:status, :submission, :messages, :steps, :cost_source,
189
191
  :input_tokens, :output_tokens, :cached_tokens, :thinking_tokens, :cost_usd,
190
- :error) do
192
+ :error, :cache_id) do
193
+ def initialize(cache_id: nil, **attributes) = super(cache_id:, **attributes)
194
+
191
195
  def success? = status == :submitted
192
196
 
193
197
  def to_h = super.merge(cost_source: cost_source&.to_h, version: Miniswen::VERSION)
@@ -205,7 +209,7 @@ module Miniswen
205
209
  input_tokens: data[:input_tokens], output_tokens: data[:output_tokens],
206
210
  cached_tokens: data[:cached_tokens], thinking_tokens: data[:thinking_tokens],
207
211
  cost_usd: data[:cost_usd],
208
- error: data[:error]
212
+ error: data[:error], cache_id: data[:cache_id]
209
213
  )
210
214
  end
211
215
 
@@ -230,7 +234,7 @@ module Miniswen
230
234
  attr_reader :messages, :environment
231
235
 
232
236
  private attr_reader :max_steps, :max_time, :max_cost, :exec_timeout, :max_output_tokens,
233
- :clock, :reporter
237
+ :cache_id, :clock, :reporter
234
238
 
235
239
  # `model` is a litellm-style name ("openrouter/z-ai/glm-5.2"), optionally
236
240
  # suffixed with a reasoning effort ("openrouter/openai/gpt-5.6-luna#xhigh").
@@ -261,25 +265,17 @@ module Miniswen
261
265
  @reporter = reporter
262
266
  end
263
267
 
264
- def run(instruction)
265
- uname = execute("uname -srvm").output.to_s.strip
266
- @messages = [
267
- { role: "system", content: SYSTEM_TEMPLATE },
268
- { role: "user", content: format(INSTANCE_TEMPLATE,
269
- instruction: instruction,
270
- system_information: uname,
271
- macos_sed_note: uname.start_with?("Darwin") ? "\n#{MACOS_SED_NOTE}" : "") }
272
- ]
273
-
274
- @steps = 0
275
- @cost = 0.0
276
-
277
- @totals = { input_tokens: 0, output_tokens: 0, cached_tokens: 0, thinking_tokens: 0 }
268
+ # With a `history` (the Result of an interrupted run), the session goes on
269
+ # where it stopped, under the budget the earlier part already spent.
270
+ def run(instruction = nil, history: nil)
271
+ if history
272
+ resume(history)
273
+ else
274
+ start(instruction)
275
+ end
278
276
 
279
- @cost_known = true
280
277
  @consecutive_format_errors = 0
281
278
  @refused_turns = 0
282
- @started_at = @clock.call
283
279
 
284
280
  loop do
285
281
  (status = limit_reached) and return finish(status)
@@ -310,7 +306,7 @@ module Miniswen
310
306
  Result.new(
311
307
  status: :error, submission: nil, messages: @messages || [], steps: @steps.to_i,
312
308
  cost_source: cost_source, cost_usd: @cost_known == false ? nil : @cost.to_f,
313
- error: error,
309
+ error: error, cache_id:,
314
310
  **(@totals || { input_tokens: 0, output_tokens: 0, cached_tokens: 0, thinking_tokens: 0 })
315
311
  )
316
312
  end
@@ -329,6 +325,51 @@ module Miniswen
329
325
 
330
326
  private
331
327
 
328
+ def start(instruction)
329
+ @cache_id = SecureRandom.uuid
330
+ uname = execute("uname -srvm").output.to_s.strip
331
+ @messages = [
332
+ { role: "system", content: SYSTEM_TEMPLATE },
333
+ { role: "user", content: format(INSTANCE_TEMPLATE,
334
+ instruction: instruction,
335
+ system_information: uname,
336
+ macos_sed_note: uname.start_with?("Darwin") ? "\n#{MACOS_SED_NOTE}" : "") }
337
+ ]
338
+
339
+ @steps = 0
340
+ @cost = 0.0
341
+ @cost_known = true
342
+ @totals = { input_tokens: 0, output_tokens: 0, cached_tokens: 0, thinking_tokens: 0 }
343
+ @started_at = @clock.call
344
+ end
345
+
346
+ def resume(history)
347
+ @cache_id = history.cache_id || SecureRandom.uuid
348
+ @messages = answered_messages(history.messages)
349
+
350
+ @steps = history.steps.to_i
351
+ @cost = history.cost_usd.to_f
352
+ @cost_known = !history.cost_usd.nil?
353
+ @totals = { input_tokens: history.input_tokens.to_i, output_tokens: history.output_tokens.to_i,
354
+ cached_tokens: history.cached_tokens.to_i, thinking_tokens: history.thinking_tokens.to_i }
355
+ @started_at = @clock.call - elapsed(history.messages)
356
+ end
357
+
358
+ # A turn whose calls did not all get a result cannot go back to a provider:
359
+ # it is dropped, and the model decides again from the tree it left.
360
+ def answered_messages(messages)
361
+ turn = messages.rindex { it[:role] == "assistant" && it[:tool_calls] }
362
+ return messages.dup unless turn
363
+
364
+ answered = messages[(turn + 1)..].filter_map { it[:tool_call_id] if it[:role] == "tool" }
365
+ messages[turn][:tool_calls].all? { answered.include?(it[:id]) } ? messages.dup : messages[0...turn]
366
+ end
367
+
368
+ def elapsed(messages)
369
+ times = messages.filter_map { Time.parse(it[:timestamp]) if it[:role] == "assistant" && it[:timestamp] }
370
+ times.empty? ? 0.0 : times.max - times.min
371
+ end
372
+
332
373
  def execute(command)
333
374
  environment.exec(command, timeout: exec_timeout, env: EXEC_ENV)
334
375
  end
@@ -409,7 +450,9 @@ module Miniswen
409
450
  error = +""
410
451
  error << "Unknown tool '#{call[:name]}'." if call[:name] != "bash"
411
452
  arguments = call[:arguments]
412
- if !arguments.is_a?(Hash) || !arguments["command"]
453
+ if arguments.is_a?(String)
454
+ error << "The bash tool call arguments are not valid JSON. Resend the call with a JSON object of the form {\"command\": \"...\"}."
455
+ elsif !arguments.is_a?(Hash) || !arguments["command"]
413
456
  error << "Missing 'command' argument in bash tool call."
414
457
  elsif arguments["command"].to_s.include?("\0")
415
458
  # Process.spawn rejects strings with NUL bytes, so the command could
@@ -497,7 +540,7 @@ module Miniswen
497
540
  def finish(status, submission: nil)
498
541
  Result.new(
499
542
  status: status, submission: submission, messages: @messages, steps: @steps,
500
- cost_source: cost_source, cost_usd: @cost_known ? @cost : nil, error: nil, **@totals
543
+ cost_source: cost_source, cost_usd: @cost_known ? @cost : nil, error: nil, cache_id:, **@totals
501
544
  )
502
545
  end
503
546
 
@@ -541,10 +584,14 @@ module Miniswen
541
584
  end
542
585
 
543
586
  def routing_params
544
- order = provider_order
545
- return {} unless order && @provider == "openrouter"
587
+ return {} unless @provider == "openrouter"
546
588
 
547
- { provider: { order: order.split(",").map(&:strip), allow_fallbacks: false } }
589
+ params = {}
590
+ params[:prompt_cache_key] = cache_id if @id.start_with?("meta/muse-")
591
+ if (order = provider_order)
592
+ params[:provider] = { order: order.split(",").map(&:strip), allow_fallbacks: false }
593
+ end
594
+ params
548
595
  end
549
596
 
550
597
  def provider_order = ENV["LEMANS_PROVIDER_ORDER"] || ENV["OPENROUTER_PROVIDER_ORDER"]
data/lib/miniswen/cli.rb CHANGED
@@ -25,6 +25,7 @@ module Miniswen
25
25
  @jail = false
26
26
  @allowed_hosts = nil
27
27
  @workdir = nil
28
+ @history = nil
28
29
  end
29
30
 
30
31
  def run
@@ -56,7 +57,7 @@ module Miniswen
56
57
  agent = Agent.new(model:, reporter:, environment:, **options)
57
58
 
58
59
  begin
59
- result = agent.run(instruction)
60
+ result = agent.run(instruction, history: @history && Agent::Result.from_h(JSON.parse(@history)))
60
61
  rescue StandardError => e
61
62
  write_results(agent.partial_result(error_message(e)))
62
63
  raise
@@ -105,7 +106,7 @@ module Miniswen
105
106
 
106
107
  def parse_args!
107
108
  parser = OptionParser.new do |opts|
108
- opts.banner = "Usage: miniswen -m MODEL -p INSTRUCTION [...options]"
109
+ opts.banner = "Usage: miniswen -m MODEL (-p INSTRUCTION | --continue-from PATH) [...options]"
109
110
 
110
111
  opts.on("-m MODEL", "--model=MODEL", String,
111
112
  "LLM to use (litellm format, e.g.: openrouter/openai/gpt-5.6-luna") do |v|
@@ -116,6 +117,10 @@ module Miniswen
116
117
  @instruction = File.file?(v) ? File.read(v) : v
117
118
  end
118
119
 
120
+ opts.on("--continue-from=PATH", String, "Continue the session saved by --results-path at PATH (no -p needed)") do |v|
121
+ @history = File.read(v)
122
+ end
123
+
119
124
  opts.on("--max-steps=STEPS", Integer, "Max steps count") do |v|
120
125
  options[:max_steps] = v
121
126
  end
@@ -197,7 +202,7 @@ module Miniswen
197
202
  return if @refresh_registry
198
203
 
199
204
  raise "Use -m to specify the model" unless @model
200
- raise "Please, provide instructions via -p option" unless @instruction
205
+ raise "Please, provide instructions via -p option" unless @instruction || @history
201
206
  end
202
207
  end
203
208
  end
@@ -20,7 +20,7 @@ module Miniswen
20
20
  env&.each { |key, value| argv += [ "--env", "#{key}=#{value}" ] }
21
21
  argv << id
22
22
  argv += [ "timeout", timeout.ceil.to_s ] if timeout&.positive?
23
- argv += [ "sh", "-c", command ]
23
+ argv += [ "bash", "-c", command ]
24
24
 
25
25
  Open3.popen2e(*argv) do |stdin, pipe, wait|
26
26
  stdin.close
data/lib/miniswen/jail.rb CHANGED
@@ -61,7 +61,7 @@ module Miniswen
61
61
  def spawn_arguments(command, env)
62
62
  [ command_env(env),
63
63
  "nsenter", "--target", @holder.pid.to_s, "--net", "--mount", "--pid=/proc/#{@holder.pid}/ns/pid_for_children", "--wd=#{@workdir}",
64
- "--", "setpriv", "--bounding-set=-all", "--inh-caps=-all", "--no-new-privs", "--", "sh", "-c", command ]
64
+ "--", "setpriv", "--bounding-set=-all", "--inh-caps=-all", "--no-new-privs", "--", "bash", "-c", command ]
65
65
  end
66
66
 
67
67
  def spawn_options = super.merge(unsetenv_others: true)
@@ -28,7 +28,7 @@ module Miniswen
28
28
 
29
29
  private
30
30
 
31
- def spawn_arguments(command, env) = [ env || {}, "sh", "-c", command ]
31
+ def spawn_arguments(command, env) = [ env || {}, "bash", "-c", command ]
32
32
 
33
33
  def spawn_options = { pgroup: true }
34
34
 
@@ -43,7 +43,20 @@ module Miniswen
43
43
  module RetryTransientFailures
44
44
  def retry_exceptions = super + [ Faraday::SSLError, Faraday::ParsingError ]
45
45
  end
46
+
47
+ # ruby_llm parses tool-call arguments while it builds the message, so one
48
+ # malformed call would raise and take the whole completion with it. The raw
49
+ # string goes through instead and bounces as a format error.
50
+ module LenientToolArguments
51
+ def parse_tool_call_arguments(tool_call)
52
+ super
53
+ rescue JSON::ParserError
54
+ tool_call.dig("function", "arguments")
55
+ end
56
+ end
46
57
  end
47
58
 
48
59
  RubyLLM::Providers::OpenRouter.prepend(Miniswen::VerbatimReasoningDetails)
49
60
  RubyLLM::Connection.prepend(Miniswen::RetryTransientFailures)
61
+ RubyLLM::Providers::OpenAI::Tools.prepend(Miniswen::LenientToolArguments)
62
+ RubyLLM::Providers::OpenAI::Tools.singleton_class.prepend(Miniswen::LenientToolArguments)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Miniswen
4
- VERSION = "1.4.0"
4
+ VERSION = "1.5.2"
5
5
  end
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.0
4
+ version: 1.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Svyatoslav Kryukov